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:
+101
-1
@@ -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
@@ -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)])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user