Inject state dicts by name (prev/state), rename MigrationResult to MigrationReport.

State dict injection now works by parameter name (prev/state, annotation
not checked) with DictPrev/DictState tags taking precedence; DictPre/DictPost
remain as aliases. MigrationResult is renamed to MigrationReport with fields
original/version/applied; the old type alias and a deprecated .migrations
property remain. Old symbols stay covered by the original tests; new-style
tests import from the kanta root. Includes some unrelated ruff formatting.
This commit is contained in:
2026-08-27 14:50:34 +00:00
parent 4ef74e027f
commit 94c5ddeaba
13 changed files with 320 additions and 128 deletions
+101 -1
View File
@@ -2,7 +2,7 @@ from typing import Any, Optional, Union
import pytest
from kanta import Kanta
from kanta import DictPrev, DictState, Kanta
from kanta.callbacks import DictPost, DictPre, LogFmt
from kanta.exceptions import DatabaseError
@@ -166,6 +166,106 @@ async def test_logfmt_injects_states(tmp_path, format_config, caplog):
assert "Alice" in caplog.text
@pytest.mark.asyncio
async def test_logfmt_injects_states_by_name(tmp_path, format_config, caplog):
import logging
caplog.set_level(logging.INFO, logger="kanta.transaction")
path = tmp_path / "test.db"
kanta = make_kanta(path, Data, format_config)
@kanta.logfmt
def resolve_users(value: str, prev, state: dict | None) -> str | None:
assert prev == {}
assert state is not None
return state.get("users", {}).get(value, {}).get("name")
await kanta.open()
with kanta.transaction(action="create_user") as data:
data.users["uuid-9"] = User(name="Carol")
await kanta.close()
assert "Carol" in caplog.text
@pytest.mark.asyncio
async def test_logfmt_state_name_ignores_annotation(tmp_path, format_config, caplog):
import logging
caplog.set_level(logging.INFO, logger="kanta.transaction")
path = tmp_path / "test.db"
kanta = make_kanta(path, Data, format_config)
# Matching by name does not check the annotation.
@kanta.logfmt
def resolve_users(value: str, state: int) -> str | None:
return state.get("users", {}).get(value, {}).get("name")
await kanta.open()
with kanta.transaction(action="create_user") as data:
data.users["uuid-10"] = User(name="Dave")
await kanta.close()
assert "Dave" in caplog.text
@pytest.mark.asyncio
async def test_logfmt_tag_takes_precedence_over_name(tmp_path, format_config, caplog):
import logging
caplog.set_level(logging.INFO, logger="kanta.transaction")
path = tmp_path / "test.db"
kanta = make_kanta(path, Data, format_config)
@kanta.logfmt
def check_prev(value: str, anything: DictPrev) -> str | None:
assert anything == {}
return None
@kanta.logfmt
def resolve_users(value: str, prev: DictState) -> str | None:
# The tag wins: prev receives the current state despite its name.
return prev.get("users", {}).get(value, {}).get("name")
await kanta.open()
with kanta.transaction(action="create_user") as data:
data.users["uuid-11"] = User(name="Erin")
await kanta.close()
assert "Erin" in caplog.text
@pytest.mark.asyncio
async def test_logfmt_class_state_attribute(tmp_path, format_config, caplog):
import logging
caplog.set_level(logging.INFO, logger="kanta.transaction")
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:
if not isinstance(value, str):
return None
return self.state.get("users", {}).get(value, {}).get("name")
await kanta.open()
with kanta.transaction(action="create_user") as data:
data.users["uuid-12"] = User(name="Fred")
await kanta.close()
assert "Fred" in caplog.text
@pytest.mark.asyncio
async def test_logfmt_class_injection(tmp_path, format_config, caplog):
import logging
+3 -4
View File
@@ -86,10 +86,9 @@ def test_cli_snapshot_line_format(tmp_path, capsys):
snapshot = Snapshot(ts=ts, v=1, m=mtime, state={"counter": 5})
change = ChangeRecord(ts=ts, a="inc", v=1, u="user1", diff={"counter": 6})
data = (
framer.frame_snapshot(serializer.encode(snapshot), record_offset=0)
+ framer.frame_change(serializer.encode(change), record_offset=0)
)
data = framer.frame_snapshot(
serializer.encode(snapshot), record_offset=0
) + framer.frame_change(serializer.encode(change), record_offset=0)
path.write_bytes(data)
code = main([str(path)])
+35
View File
@@ -721,6 +721,41 @@ async def test_logmigr_callback_replaces_default_logging(
assert not info_messages
@pytest.mark.asyncio
async def test_logmigr_callback_report(tmp_path, format_config, caplog):
import logging
from kanta import MigrationReport
path = tmp_path / "test.db"
seed_single_change(path, fixed_change("init", {"counter": 0}), format_config)
mod = type(sys)("test_migrations_report")
def migrate_v1(d):
"""Bump counter."""
d["counter"] = 2
mod.__dict__["migrate_v1"] = migrate_v1
reports = []
kanta = make_kanta(path, Data, format_config, migrations=mod)
@kanta.logmigr
def collect(report: MigrationReport):
reports.append(report)
with caplog.at_level(logging.INFO, logger="kanta.migration"):
await kanta.open()
await kanta.close()
assert len(reports) == 1
assert reports[0].original == 0
assert reports[0].version == 1
assert [m.name for m in reports[0].applied] == ["migrate_v1"]
@pytest.mark.asyncio
async def test_transaction_log_false_suppresses_log(tmp_path, format_config, caplog):
path = tmp_path / "test.db"
+19
View File
@@ -206,3 +206,22 @@ def test_description_defaults_to_version_when_no_docstring():
result = reg.apply({}, current_version=0, kanta=kanta)
assert result.migrations[0].description == "v1"
def test_report_fields():
from kanta import MigrationReport
reg = Migrations()
kanta = _DummyKanta()
@reg.register
def migrate_v1(d):
d["x"] = 1
report = reg.apply({"x": 0}, current_version=0, kanta=kanta)
assert isinstance(report, MigrationReport)
assert report.original == 0
assert report.version == 1
assert [m.name for m in report.applied] == ["migrate_v1"]
# Deprecated alias still works.
assert report.migrations is report.applied