From 799b2438be32e22cb65b8aeff2b1c33b9dc51441 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 7 Aug 2026 05:57:43 +0000 Subject: [PATCH] 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. --- docs/database.md | 12 +++++++----- kanta/kantaimpl.py | 4 ++++ kanta/logging.py | 10 ++++++++++ kanta/transaction.py | 2 ++ tests/test_logemit.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/docs/database.md b/docs/database.md index 35fe7b3..60ffa39 100644 --- a/docs/database.md +++ b/docs/database.md @@ -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: `[ ][ by ]` for changes, `[ by ] transaction aborted: ` 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. diff --git a/kanta/kantaimpl.py b/kanta/kantaimpl.py index a598119..7888ba5 100644 --- a/kanta/kantaimpl.py +++ b/kanta/kantaimpl.py @@ -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, diff --git a/kanta/logging.py b/kanta/logging.py index 293484f..18c4a08 100644 --- a/kanta/logging.py +++ b/kanta/logging.py @@ -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}" diff --git a/kanta/transaction.py b/kanta/transaction.py index 5dd8509..f947bed 100644 --- a/kanta/transaction.py +++ b/kanta/transaction.py @@ -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, diff --git a/tests/test_logemit.py b/tests/test_logemit.py index 311f39a..a4218e7 100644 --- a/tests/test_logemit.py +++ b/tests/test_logemit.py @@ -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