Kanta colors log header parts: extra is a plain string, logheader callbacks receive pre-colored parts

This commit is contained in:
Leo Vasanko
2026-08-07 00:31:04 +00:00
parent ec08c185aa
commit e0725c5738
7 changed files with 173 additions and 120 deletions
+66 -42
View File
@@ -4,10 +4,21 @@ import pytest
from kanta import Kanta
from kanta.callbacks import DictPost, DictPre
from kanta.logging import _ACTION, _RESET, _TARGET, _USER, configure_logging
from .support import Data, make_kanta, read_changes
@pytest.fixture(autouse=True)
def _reset_kanta_loggers():
yield
for name in ("kanta", "kanta.transaction", "kanta.bootstrap", "kanta.migration"):
logger = logging.getLogger(name)
logger.setLevel(logging.NOTSET)
logger.propagate = True
logger.handlers.clear()
def test_logheader_rejects_async(tmp_path, format_config):
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
@@ -38,59 +49,74 @@ def test_logheader_rejects_unknown_annotation(tmp_path, format_config):
return action
def test_logheader_rejects_dict_extra(tmp_path, format_config):
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
with pytest.raises(TypeError, match="unsupported annotation"):
@kanta.logheader
def header(action: str, extra: dict) -> str:
return action
@pytest.mark.asyncio
async def test_logheader_replaces_default_header(tmp_path, format_config, caplog):
caplog.set_level(logging.INFO, logger="kanta.transaction")
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
@kanta.logheader
def header(action: str, user: str | None, extra: dict | None) -> str:
return f"HDR {action} user={user} session={extra['session']}"
def header(action: str, user: str, extra: str) -> str:
return f"{user} {action} {extra}"
await kanta.open(log=False)
with kanta.transaction(action="update", user="alice", extra={"session": 3}) as data:
with kanta.transaction(action="update", user="alice", extra="tgt") as data:
data.counter = 1
await kanta.close()
assert "HDR update user=alice session=3" in caplog.text
# caplog strips ANSI codes; the colors are verified via capsys below.
assert "alice update tgt" in caplog.text
# The diff body is still logged after the custom header.
assert "counter" in caplog.text
@pytest.mark.asyncio
async def test_logheader_default_target_is_filename(tmp_path, format_config, caplog):
caplog.set_level(logging.INFO, logger="kanta.transaction")
kanta = make_kanta(tmp_path / "mydb.db", Data, format_config)
async def test_logheader_parts_are_colored_by_kanta(tmp_path, format_config, capsys):
logging.getLogger("kanta").handlers.clear()
configure_logging()
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
@kanta.logheader
def header(action: str, extra: dict | None) -> str:
return f"target={extra['target']}"
def header(action: str, user: str, extra: str) -> str:
return f"{user} {action} {extra}"
await kanta.open(log=False)
with kanta.transaction(action="update", user="alice", extra="tgt") as data:
data.counter = 1
await kanta.close()
err = capsys.readouterr().err
assert f"{_USER}alice{_RESET}" in err
assert f"{_ACTION}update{_RESET}" in err
assert f"{_TARGET}tgt{_RESET}" in err
@pytest.mark.asyncio
async def test_logheader_missing_user_and_extra_are_empty(
tmp_path, format_config, caplog
):
caplog.set_level(logging.INFO, logger="kanta.transaction")
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
@kanta.logheader
def header(action: str, user: str, extra: str) -> str:
return f"<{user}><{extra}>"
await kanta.open(log=False)
with kanta.transaction(action="update") as data:
data.counter = 1
await kanta.close()
assert "target=mydb.db" in caplog.text
@pytest.mark.asyncio
async def test_logheader_explicit_target_kept(tmp_path, format_config, caplog):
caplog.set_level(logging.INFO, logger="kanta.transaction")
kanta = make_kanta(tmp_path / "mydb.db", Data, format_config)
@kanta.logheader
def header(action: str, extra: dict | None) -> str:
return f"target={extra['target']}"
await kanta.open(log=False)
with kanta.transaction(
action="update", extra={"target": "Project X (abcd1234)"}
) as data:
data.counter = 1
await kanta.close()
assert "target=Project X (abcd1234)" in caplog.text
assert "<><>" in caplog.text
@pytest.mark.asyncio
@@ -106,7 +132,7 @@ async def test_logheader_injects_states_and_kanta(tmp_path, format_config, caplo
kanta: Kanta,
) -> str:
return (
f"{action} counter {previous.get('counter')}"
f"counter {previous.get('counter')}"
f" -> {current.get('counter')} db={kanta.filename.name}"
)
@@ -115,7 +141,7 @@ async def test_logheader_injects_states_and_kanta(tmp_path, format_config, caplo
data.counter = 5
await kanta.close()
assert "increment counter 0 -> 5 db=test.db" in caplog.text
assert "counter 0 -> 5 db=test.db" in caplog.text
@pytest.mark.asyncio
@@ -128,15 +154,15 @@ async def test_logheader_chain_first_non_none_wins(tmp_path, format_config, capl
return None
@kanta.logheader
def second(action: str) -> str:
return f"SECOND {action}"
def second(action: str, extra: str) -> str:
return f"SECOND {extra}"
await kanta.open(log=False)
with kanta.transaction(action="update") as data:
with kanta.transaction(action="update", extra="marked") as data:
data.counter = 1
await kanta.close()
assert "SECOND update" in caplog.text
assert "SECOND marked" in caplog.text
@pytest.mark.asyncio
@@ -169,7 +195,7 @@ async def test_logheader_receives_formatted_user(tmp_path, format_config, caplog
return "Alice"
@kanta.logheader
def header(action: str, user: str | None) -> str:
def header(action: str, user: str) -> str:
return f"actor={user}"
await kanta.open(log=False)
@@ -186,13 +212,13 @@ async def test_logheader_applies_to_bootstrap(tmp_path, format_config, caplog):
kanta = make_kanta(tmp_path / "test.db", Data, format_config)
@kanta.logheader
def header(action: str, extra: dict | None) -> str:
return f"BOOT {action} target={extra['target']}"
def header(action: str) -> str:
return f"BOOT {action}"
await kanta.open()
await kanta.close()
assert "BOOT bootstrap target=test.db" in caplog.text
assert "BOOT bootstrap" in caplog.text
@pytest.mark.asyncio
@@ -217,9 +243,7 @@ async def test_extra_is_not_persisted(tmp_path, format_config):
kanta = make_kanta(path, Data, format_config)
await kanta.open(log=False)
with kanta.transaction(
action="update", user="alice", extra={"session": 3, "target": "X"}
) as data:
with kanta.transaction(action="update", user="alice", extra="session-3") as data:
data.counter = 1
await kanta.close()