Improved log formatting support by @kanta.logfmt, which replaces old resolver and user_display arguments (breaking change).
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from kanta import Kanta
|
||||
from kanta.callbacks import DictPost, DictPre, LogFmt
|
||||
from kanta.exceptions import DatabaseError
|
||||
|
||||
from .support import Data, User, make_kanta
|
||||
|
||||
|
||||
def test_bootstrap_rejects_unannotated_param(tmp_path, format_config):
|
||||
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
|
||||
|
||||
with pytest.raises(TypeError, match="without an annotation or default"):
|
||||
|
||||
@kanta.bootstrap
|
||||
def seed(data):
|
||||
data.counter = 1
|
||||
|
||||
|
||||
def test_bootstrap_accepts_unknown_with_default(tmp_path, format_config):
|
||||
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
|
||||
|
||||
@kanta.bootstrap
|
||||
def seed(data: Data, extra: int = 0) -> None:
|
||||
data.counter = extra + 1
|
||||
|
||||
# Should register without error.
|
||||
|
||||
|
||||
def test_bootstrap_rejects_unknown_annotation(tmp_path, format_config):
|
||||
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
|
||||
|
||||
with pytest.raises(TypeError, match="unsupported annotation"):
|
||||
|
||||
@kanta.bootstrap
|
||||
def seed(data: int):
|
||||
pass
|
||||
|
||||
|
||||
def test_logfmt_requires_value_annotation(tmp_path, format_config):
|
||||
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
|
||||
|
||||
with pytest.raises(TypeError, match="value parameter.*must be annotated"):
|
||||
|
||||
@kanta.logfmt
|
||||
def resolve_names(previous: DictPre, current: DictPost) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def test_logfmt_requires_return_annotation(tmp_path, format_config):
|
||||
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
|
||||
|
||||
with pytest.raises(TypeError, match="must annotate its return"):
|
||||
|
||||
@kanta.logfmt
|
||||
def resolve_names(value: str, current: DictPost):
|
||||
return None
|
||||
|
||||
|
||||
def test_logfmt_rejects_async_callback(tmp_path, format_config):
|
||||
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
|
||||
|
||||
with pytest.raises(TypeError, match="must not be async"):
|
||||
|
||||
@kanta.logfmt
|
||||
async def resolve_names(value: str, current: DictPost) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bootstrap_injects_data_by_type(tmp_path, format_config):
|
||||
path = tmp_path / "test.db"
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.bootstrap
|
||||
def seed(data: Data) -> None:
|
||||
data.counter = 7
|
||||
|
||||
await kanta.open()
|
||||
assert kanta.data.counter == 7
|
||||
await kanta.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bootstrap_injects_kanta(tmp_path, format_config):
|
||||
path = tmp_path / "test.db"
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
seen: list[Kanta] = []
|
||||
|
||||
@kanta.bootstrap
|
||||
def seed(data: Data, kanta_ref: Kanta) -> None:
|
||||
seen.append(kanta_ref)
|
||||
data.counter = 8
|
||||
|
||||
await kanta.open()
|
||||
assert seen == [kanta]
|
||||
assert kanta.data.counter == 8
|
||||
await kanta.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logfmt_injects_states(tmp_path, format_config, caplog):
|
||||
import logging
|
||||
|
||||
caplog.set_level(logging.INFO, logger="kanta.changes")
|
||||
path = tmp_path / "test.db"
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.logfmt
|
||||
def resolve_users(value: str, current: DictPost) -> str | None:
|
||||
return current.get("users", {}).get(value, {}).get("name")
|
||||
|
||||
await kanta.open()
|
||||
|
||||
with kanta.transaction(action="create_user") as data:
|
||||
data.users["uuid-1"] = User(name="Alice")
|
||||
|
||||
await kanta.close()
|
||||
|
||||
assert "Alice" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logfmt_class_injection(tmp_path, format_config, caplog):
|
||||
import logging
|
||||
|
||||
caplog.set_level(logging.INFO, logger="kanta.changes")
|
||||
path = tmp_path / "test.db"
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.logfmt
|
||||
class UserLogFmt(LogFmt):
|
||||
def resolve(self, value: str, path: str) -> str | None:
|
||||
return self.current_state.get("users", {}).get(value, {}).get("name")
|
||||
|
||||
await kanta.open()
|
||||
|
||||
with kanta.transaction(action="create_user") as data:
|
||||
data.users["uuid-2"] = User(name="Bob")
|
||||
|
||||
await kanta.close()
|
||||
|
||||
assert "Bob" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_logfmt_chain(tmp_path, format_config, caplog):
|
||||
import logging
|
||||
|
||||
caplog.set_level(logging.INFO, logger="kanta.changes")
|
||||
path = tmp_path / "test.db"
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.logfmt
|
||||
def resolve_a(value: str) -> str | None:
|
||||
return "A" if value == "a" else None
|
||||
|
||||
@kanta.logfmt
|
||||
def resolve_b(value: str) -> str | None:
|
||||
return "B" if value == "b" else None
|
||||
|
||||
await kanta.open()
|
||||
|
||||
with kanta.transaction(action="create_user") as data:
|
||||
data.users["a"] = User(name="first")
|
||||
data.users["b"] = User(name="second")
|
||||
|
||||
await kanta.close()
|
||||
|
||||
assert "A" in caplog.text
|
||||
assert "B" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logfmt_path_context(tmp_path, format_config, caplog):
|
||||
import logging
|
||||
|
||||
caplog.set_level(logging.INFO, logger="kanta.changes")
|
||||
path = tmp_path / "test.db"
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.logfmt(path="users.uuid-1")
|
||||
def resolve_user_key(value: str) -> str | None:
|
||||
if value == "uuid-1":
|
||||
return "user-alice"
|
||||
return None
|
||||
|
||||
await kanta.open()
|
||||
|
||||
with kanta.transaction(action="create_user") as data:
|
||||
data.users["uuid-1"] = User(name="Alice")
|
||||
|
||||
await kanta.close()
|
||||
|
||||
assert "user-alice" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logfmt_decorator_path_filters_calls(tmp_path, format_config, caplog):
|
||||
import logging
|
||||
|
||||
caplog.set_level(logging.INFO, logger="kanta.changes")
|
||||
path = tmp_path / "test.db"
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.logfmt(path="counter")
|
||||
def fmt_counter(value: Any) -> str | None:
|
||||
if value == 1:
|
||||
return "one"
|
||||
return None
|
||||
|
||||
await kanta.open()
|
||||
|
||||
with kanta.transaction(action="create_user") as data:
|
||||
data.users["uuid-1"] = User(name="Alice")
|
||||
data.counter = 1
|
||||
|
||||
await kanta.close()
|
||||
|
||||
assert "one" in caplog.text
|
||||
assert "uuid-1" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logfmt_user_path_replaces_user_display(tmp_path, format_config, caplog):
|
||||
import logging
|
||||
|
||||
caplog.set_level(logging.INFO, logger="kanta.changes")
|
||||
path = tmp_path / "test.db"
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.logfmt(path="$user")
|
||||
def resolve_user(value: str, current: DictPost) -> str | None:
|
||||
return current.get("users", {}).get(value, {}).get("name")
|
||||
|
||||
await kanta.open()
|
||||
|
||||
with kanta.transaction(action="create_user", user="uuid-1") as data:
|
||||
data.users["uuid-1"] = User(name="Alice")
|
||||
|
||||
await kanta.close()
|
||||
|
||||
assert "by Alice" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logfmt_non_string_value(tmp_path, format_config, caplog):
|
||||
import logging
|
||||
|
||||
caplog.set_level(logging.INFO, logger="kanta.changes")
|
||||
path = tmp_path / "test.db"
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.logfmt
|
||||
def fmt_count(value: Any, path: str) -> str | None:
|
||||
if path == "counter" and value == 1:
|
||||
return "one"
|
||||
return None
|
||||
|
||||
await kanta.open()
|
||||
|
||||
with kanta.transaction(action="inc") as data:
|
||||
data.counter = 1
|
||||
|
||||
await kanta.close()
|
||||
|
||||
assert "one" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fatal_error_injects_kanta_and_error(
|
||||
tmp_path, format_config, monkeypatch
|
||||
):
|
||||
import asyncio
|
||||
|
||||
path = tmp_path / "test.db"
|
||||
errors: list[DatabaseError] = []
|
||||
kantas: list[Kanta] = []
|
||||
signaled = asyncio.Event()
|
||||
|
||||
kanta = make_kanta(path, Data, format_config, flush_interval=0.01)
|
||||
|
||||
@kanta.fatal_error
|
||||
def on_fatal(error: DatabaseError, kanta_ref: Kanta) -> None:
|
||||
errors.append(error)
|
||||
kantas.append(kanta_ref)
|
||||
signaled.set()
|
||||
|
||||
await kanta.open()
|
||||
|
||||
with kanta.transaction(action="inc") as data:
|
||||
data.counter = 1
|
||||
|
||||
def fail_write(_data: bytes) -> None:
|
||||
raise OSError("simulated background write failure")
|
||||
|
||||
monkeypatch.setattr(kanta._impl.file, "write", fail_write)
|
||||
|
||||
await asyncio.wait_for(signaled.wait(), timeout=1.0)
|
||||
assert errors
|
||||
assert kantas == [kanta]
|
||||
|
||||
await kanta.close()
|
||||
@@ -16,10 +16,34 @@ def test_delete():
|
||||
assert any("old_key" in line for line in lines)
|
||||
|
||||
|
||||
def test_resolver():
|
||||
def test_logfmt():
|
||||
lines = format_diff(
|
||||
{"users": {"uuid-1": {"name": "Alice"}}},
|
||||
previous={},
|
||||
resolver=lambda x: "Alice" if x == "uuid-1" else x,
|
||||
logfmt=lambda value, path: "Alice" if value == "uuid-1" else None,
|
||||
)
|
||||
assert any("Alice" in line for line in lines)
|
||||
|
||||
|
||||
def test_logfmt_uses_path_context():
|
||||
lines = format_diff(
|
||||
{
|
||||
"users": {"uuid-1": {"name": "Alice"}},
|
||||
"groups": {"uuid-1": {"name": "Admins"}},
|
||||
},
|
||||
previous={},
|
||||
logfmt=lambda value, path: (
|
||||
"User Alice" if path.startswith("users.") and value == "uuid-1" else None
|
||||
),
|
||||
)
|
||||
assert any("User Alice" in line for line in lines)
|
||||
assert any("uuid-1" in line for line in lines)
|
||||
|
||||
|
||||
def test_logfmt_formats_non_string_value():
|
||||
lines = format_diff(
|
||||
{"count": 42},
|
||||
previous={},
|
||||
logfmt=lambda value, path: "forty-two" if value == 42 else None,
|
||||
)
|
||||
assert any("forty-two" in line for line in lines)
|
||||
|
||||
@@ -106,7 +106,7 @@ async def test_bootstrap_decorator_with_args(tmp_path, format_config):
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.bootstrap(action="seed_init", user="system")
|
||||
def seed(data):
|
||||
def seed(data: Data):
|
||||
data.counter = 3
|
||||
|
||||
await kanta.open()
|
||||
@@ -121,7 +121,7 @@ async def test_bootstrap_decorator_without_args(tmp_path, format_config):
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.bootstrap
|
||||
def seed(data):
|
||||
def seed(data: Data):
|
||||
data.counter = 4
|
||||
|
||||
await kanta.open()
|
||||
@@ -136,7 +136,7 @@ async def test_bootstrap_decorator_async(tmp_path, format_config):
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.bootstrap(action="async_seed")
|
||||
async def seed(data):
|
||||
async def seed(data: Data):
|
||||
await asyncio.sleep(0)
|
||||
data.counter = 5
|
||||
|
||||
@@ -152,11 +152,11 @@ async def test_bootstrap_decorator_multiple_handlers_in_order(tmp_path, format_c
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.bootstrap(action="boot_1")
|
||||
def seed_one(data):
|
||||
def seed_one(data: Data):
|
||||
data.counter = 1
|
||||
|
||||
@kanta.bootstrap(action="boot_2")
|
||||
async def seed_two(data):
|
||||
async def seed_two(data: Data):
|
||||
await asyncio.sleep(0)
|
||||
data.counter = 2
|
||||
|
||||
@@ -172,7 +172,7 @@ async def test_bootstrap_failure_removes_database_file(tmp_path, format_config):
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.bootstrap(action="boot_fail")
|
||||
def seed_fail(data):
|
||||
def seed_fail(data: Data):
|
||||
data.counter = 10
|
||||
raise RuntimeError("bootstrap failed")
|
||||
|
||||
@@ -188,7 +188,7 @@ async def test_bootstrap_async_failure_removes_database_file(tmp_path, format_co
|
||||
kanta = make_kanta(path, Data, format_config)
|
||||
|
||||
@kanta.bootstrap(action="boot_fail_async")
|
||||
async def seed_fail(data):
|
||||
async def seed_fail(data: Data):
|
||||
await asyncio.sleep(0)
|
||||
data.counter = 10
|
||||
raise RuntimeError("bootstrap async failed")
|
||||
|
||||
Reference in New Issue
Block a user