Add kanta instance to LogEvent and make header settable

Every emitted event now carries the originating Kanta instance so logemit
callbacks can reach application state attached to it. The header property
gains a setter, formalizing restyle-then-delegate: assign ev.header and
return truthy to keep the default diff routing with a custom header.
This commit is contained in:
Leo Vasanko
2026-08-07 05:57:43 +00:00
parent c90db530fd
commit 799b2438be
5 changed files with 52 additions and 5 deletions
+7 -5
View File
@@ -231,16 +231,18 @@ def resolve_user_key(value: str) -> str | None:
mechanism: when no `logemit` callback handles an event,
`kanta.logging.default_emit` renders it with the built-in formatting.
- A `LogEvent` carries the event `kind` (`"change"`, `"created"`,
`"migrated"`, `"aborted"`), the preferred `logger` and `level`, and all
relevant state: `action`, `user`, `extra`, `error` (for aborted
transactions), `diff`, `previous`/`current` state dicts, the built `logfmt`
chain, and version info for migration events.
`"migrated"`, `"aborted"`), the preferred `logger` and `level`, the
`kanta` instance, and all relevant state: `action`, `user`, `extra`,
`error` (for aborted transactions), `diff`, `previous`/`current` state
dicts, the built `logfmt` chain, and version info for migration events.
- The built-in formatting is assembled from standard blocks that custom
emitters can reuse as-is or replace piecemeal:
- `event.header` — a lazy property producing the default one-line header
for any kind: `<action>[ <extra>][ by <user>]` for changes,
`<action>[ by <user>] transaction aborted: <error>` for aborts, and the
plain `Created`/`Migrated` summaries.
plain `Created`/`Migrated` summaries. It is settable: assign
`event.header = ...` and return truthy to restyle the header while
keeping the default diff routing.
- `event.diff_lines` — a lazy property producing the pretty diff body for
change events (built only if accessed).
- `default_emit` itself is just `header` plus the `diff_lines` routing.
+4
View File
@@ -131,6 +131,7 @@ class KantaImpl(PersistenceMixin, Generic[T]):
kind="change",
logger=migration_log,
level=logging.DEBUG,
kanta=self._kanta,
action=info.name,
diff=info.diff,
previous=info.before,
@@ -143,6 +144,7 @@ class KantaImpl(PersistenceMixin, Generic[T]):
LogEvent(
kind="migrated",
logger=migration_log,
kanta=self._kanta,
filename=str(self.filename),
from_version=previous_version,
to_version=migration_result.version,
@@ -324,6 +326,7 @@ class KantaImpl(PersistenceMixin, Generic[T]):
LogEvent(
kind="created",
logger=logger,
kanta=self._kanta,
filename=str(self.filename.resolve()),
),
self.callback_registry.logemit_handlers,
@@ -344,6 +347,7 @@ class KantaImpl(PersistenceMixin, Generic[T]):
LogEvent(
kind="change",
logger=logger,
kanta=self._kanta,
action=self.bootstrap_action,
user=formatted_user,
diff=record.diff,
+10
View File
@@ -52,6 +52,7 @@ class LogEvent(msgspec.Struct, kw_only=True):
kind: str
logger: logging.Logger
level: int = logging.INFO
kanta: Any = None
action: str | None = None
user: str | None = None
extra: str | None = None
@@ -80,6 +81,15 @@ class LogEvent(msgspec.Struct, kw_only=True):
self._header = self._build_header()
return self._header
@header.setter
def header(self, value: str) -> None:
"""Override the header, keeping the default diff routing.
A logemit callback can restyle the header and return a truthy value:
:func:`default_emit` then logs this header instead of building one.
"""
self._header = value
def _build_header(self) -> str:
if self.kind == "created":
return f"Created {self.filename}"
+2
View File
@@ -99,6 +99,7 @@ def transaction(
LogEvent(
kind="change",
logger=logger,
kanta=impl._kanta,
action=action,
user=_resolve_user(logfmt, user),
extra=extra,
@@ -120,6 +121,7 @@ def transaction(
kind="aborted",
logger=transaction_logger,
level=logging.WARNING,
kanta=impl._kanta,
action=action,
user=resolved_user,
error=exc,
+29
View File
@@ -315,3 +315,32 @@ def test_event_header_covers_all_kinds():
)
assert "transaction aborted: boom" in aborted.header
assert "alice" in aborted.header
@pytest.mark.asyncio
async def test_event_carries_kanta_instance(tmp_path, format_config):
path = tmp_path / "test.db"
kanta = make_kanta(path, Data, format_config)
events = []
kanta.logemit(lambda ev: events.append(ev) or True)
await kanta.open()
with kanta.transaction(action="inc") as data:
data.counter = 1
await kanta.close()
assert events
assert all(ev.kanta is kanta for ev in events)
def test_header_is_settable_and_used_by_default_emit(capsys):
logging.getLogger("kanta").handlers.clear()
configure_logging()
def restyle(ev):
ev.header = f"CUSTOM {ev.action}"
return True
emit_event(_change_event(diff={"counter": 1}, previous={}), [restyle])
err = capsys.readouterr().err
assert "CUSTOM update" in err
assert "counter" in err # default diff routing still applies