diff --git a/kanta/__init__.py b/kanta/__init__.py index c8e6419..ee4b1b5 100644 --- a/kanta/__init__.py +++ b/kanta/__init__.py @@ -1,4 +1,5 @@ from .callbacks import DictPrev, DictState, LogFmt +from .diff import diff, patch from .exceptions import DatabaseError from .kanta import Kanta from .logging import LogEvent, configure_logging @@ -8,6 +9,8 @@ __all__ = [ "Kanta", "DatabaseError", "configure_logging", + "diff", + "patch", # Callback argument types "DictPrev", "DictState", diff --git a/kanta/diff.py b/kanta/diff.py index ce7d36c..5f5dc8d 100644 --- a/kanta/diff.py +++ b/kanta/diff.py @@ -49,16 +49,16 @@ def _diff(previous, current): return current -def compute_diff(previous: dict, current: dict) -> dict | None: +def diff(previous: dict, current: dict) -> dict | None: """Compute a marshaled diff between two state dicts. Returns None if there is no difference. """ - diff = _diff(previous, current) - return diff if diff is not _UNCHANGED else None + result = _diff(previous, current) + return result if result is not _UNCHANGED else None -def patch_state(state: dict, diff: dict) -> dict: +def patch(state: dict, diff: dict) -> dict: """Apply a marshaled diff to a state dict.""" return apply_diff(state, diff) diff --git a/kanta/migrations.py b/kanta/migrations.py index e969990..60c0487 100644 --- a/kanta/migrations.py +++ b/kanta/migrations.py @@ -13,7 +13,7 @@ from dataclasses import dataclass from types import ModuleType from typing import Any -from kanta.diff import compute_diff +from kanta.diff import diff from kanta.exceptions import DatabaseError # Cache registries by imported module object so that many Kanta instances using @@ -179,7 +179,7 @@ class Migrations: self._call_migration(fn, data_dict, kanta) current_version = version changed = before != data_dict - diff = compute_diff(before, data_dict) if changed else None + delta = diff(before, data_dict) if changed else None desc = (fn.__doc__ or f"v{version}").split("\n")[0].rstrip(".") migrations.append( MigrationInfo( @@ -187,7 +187,7 @@ class Migrations: description=desc, version=version, changed=changed, - diff=diff, + diff=delta, before=before, ) ) diff --git a/kanta/persistence.py b/kanta/persistence.py index 81b0436..196314b 100644 --- a/kanta/persistence.py +++ b/kanta/persistence.py @@ -13,7 +13,7 @@ from pathlib import Path from typing import Any from kanta.callbacks import CallbackRegistry, InjectionContext -from kanta.diff import compute_diff +from kanta.diff import diff from kanta.exceptions import DatabaseError, DataIntegrityError from kanta.filelock import LockedFile from kanta.structs import ChangeRecord @@ -158,11 +158,11 @@ class PersistenceMixin: The queued :class:`ChangeRecord`, or ``None`` if the diff was empty and *force* is ``False``. """ - diff = compute_diff(self.statedict, current) - if not diff: + delta = diff(self.statedict, current) + if not delta: if not force: return None - diff = {} + delta = {} # The clock is only read when a record is actually queued. now = self.now() @@ -182,7 +182,7 @@ class PersistenceMixin: v=self.version, u=user, m=m, - diff=diff, + diff=delta, ) self.pending_changes.append(record) self.statedict = copy.deepcopy(current) diff --git a/kanta/replaylog.py b/kanta/replaylog.py index a31c46f..d6013ae 100644 --- a/kanta/replaylog.py +++ b/kanta/replaylog.py @@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Any, Union import msgspec from kanta.callbacks import InjectionContext -from kanta.diff import patch_state +from kanta.diff import patch from kanta.exceptions import ReplayError from kanta.logging import _USER_PATH, LogEvent, transaction_logger from kanta.structs import ChangeRecord, Snapshot @@ -116,7 +116,7 @@ def scan_events(content: bytes, kanta: Kanta[Any]) -> tuple[list[Event], int]: events.append(SnapshotEvent(line_number, byte_pos, record_index, snap)) else: record = impl.serializer.decode(payload, type=ChangeRecord) - state = patch_state(state, record.diff) + state = patch(state, record.diff) events.append(ChangeEvent(line_number, byte_pos, record_index, record)) change_count += 1 except msgspec.DecodeError as exc: @@ -146,7 +146,7 @@ def replay_events( yield event, None, state else: previous = copy.deepcopy(state) - state = patch_state(state, event.record.diff) + state = patch(state, event.record.diff) yield event, previous, state diff --git a/kanta/transaction.py b/kanta/transaction.py index 1747eeb..d69b410 100644 --- a/kanta/transaction.py +++ b/kanta/transaction.py @@ -7,7 +7,7 @@ from contextlib import contextmanager from datetime import datetime from typing import Any -from kanta.diff import compute_diff +from kanta.diff import diff from kanta.exceptions import DataIntegrityError from kanta.callbacks import InjectionContext from kanta.logging import _USER_PATH, LogEvent, emit_event, transaction_logger @@ -65,19 +65,19 @@ def transaction( if current_dict != impl.statedict: is_bootstrap = action in {"bootstrap"} if not (is_bootstrap and not impl.statedict): - diff = compute_diff(impl.statedict, current_dict) - if diff: + delta = diff(impl.statedict, current_dict) + if delta: _logger.critical( "Database state modified outside of transaction! " "This indicates a bug where changes occurred without a transaction wrapper.\n" "Changes detected: %s", - diff, + delta, ) raise DataIntegrityError( "Database state modified outside of transaction", db_path=impl.db_path, action=action, - diff=diff, + diff=delta, ) impl.in_transaction = True @@ -86,8 +86,8 @@ def transaction( try: yield impl.data new_dict = struct_to_dict(impl.data, serializer=impl.serializer) - diff = compute_diff(impl.statedict, new_dict) - if diff: + delta = diff(impl.statedict, new_dict) + if delta: if impl.callback_registry.has("validate"): impl.callback_registry.invoke_sync( "validate", diff --git a/tests/test_diff.py b/tests/test_diff.py index c92dbce..ef6c0df 100644 --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -3,7 +3,7 @@ jsondiff is a dev dependency used only here, to verify that: - jsondiff.patch(..., marshal=True) can apply patches produced by - compute_diff (our format is a subset of jsondiff's marshaled syntax); + diff (our format is a subset of jsondiff's marshaled syntax); - apply_diff can apply patches produced by jsondiff.diff(..., marshal=True), including positional $insert/$delete list edits and per-index nested diffs. """ @@ -11,88 +11,88 @@ jsondiff is a dev dependency used only here, to verify that: import jsondiff import pytest -from kanta.diff import compute_diff, patch_state +from kanta.diff import diff, patch from kanta.logging import format_diff from kanta.serialization.base import apply_diff -# --- Producer: compute_diff ------------------------------------------------ +# --- Producer: diff ------------------------------------------------ def test_no_diff(): - assert compute_diff({"a": 1}, {"a": 1}) is None - assert compute_diff({}, {}) is None + assert diff({"a": 1}, {"a": 1}) is None + assert diff({}, {}) is None def test_simple_diff(): - diff = compute_diff({"a": 1}, {"a": 2}) - assert diff is not None - assert diff == {"a": 2} + delta = diff({"a": 1}, {"a": 2}) + assert delta is not None + assert delta == {"a": 2} def test_nested_diff(): - diff = compute_diff({"x": {"y": 1}}, {"x": {"y": 2}}) - assert diff == {"x": {"y": 2}} + delta = diff({"x": {"y": 1}}, {"x": {"y": 2}}) + assert delta == {"x": {"y": 2}} def test_key_added(): - assert compute_diff({"a": 1}, {"a": 1, "b": 2}) == {"b": 2} + assert diff({"a": 1}, {"a": 1, "b": 2}) == {"b": 2} def test_key_removed(): - assert compute_diff({"a": 1, "b": 2}, {"a": 1}) == {"$delete": ["b"]} + assert diff({"a": 1, "b": 2}, {"a": 1}) == {"$delete": ["b"]} def test_last_key_removed_is_delete_not_replace(): # jsondiff's minimal-diff search emits {"$replace": {}} here; we emit # what actually happened: the key was deleted. - assert compute_diff({"a": 1}, {}) == {"$delete": ["a"]} - assert compute_diff({"x": {"y": 1}}, {"x": {}}) == {"x": {"$delete": ["y"]}} + assert diff({"a": 1}, {}) == {"$delete": ["a"]} + assert diff({"x": {"y": 1}}, {"x": {}}) == {"x": {"$delete": ["y"]}} def test_list_changes_are_full_assignment(): # No $insert/$delete positional edits: lists are replaced wholesale. - assert compute_diff({"l": [1, 2]}, {"l": [1, 2, 3]}) == {"l": [1, 2, 3]} - assert compute_diff({"l": [1, 2, 3]}, {"l": [1, 3]}) == {"l": [1, 3]} - assert compute_diff({"l": [1]}, {"l": []}) == {"l": []} + assert diff({"l": [1, 2]}, {"l": [1, 2, 3]}) == {"l": [1, 2, 3]} + assert diff({"l": [1, 2, 3]}, {"l": [1, 3]}) == {"l": [1, 3]} + assert diff({"l": [1]}, {"l": []}) == {"l": []} def test_list_with_unchanged_prefix_is_full_assignment(): - diff = compute_diff({"l": ["a", "b", "c"]}, {"l": ["a", "x", "b", "c"]}) - assert diff == {"l": ["a", "x", "b", "c"]} + delta = diff({"l": ["a", "b", "c"]}, {"l": ["a", "x", "b", "c"]}) + assert delta == {"l": ["a", "x", "b", "c"]} def test_type_changes_are_full_assignment(): - assert compute_diff({"a": {"x": 1}}, {"a": [1]}) == {"a": [1]} + assert diff({"a": {"x": 1}}, {"a": [1]}) == {"a": [1]} # A dict replacing a non-dict is a plain assignment too: the consumer # sees from the old value whether to patch (dict) or replace. - assert compute_diff({"a": [1]}, {"a": {"x": 1}}) == {"a": {"x": 1}} - assert compute_diff({"a": 1}, {"a": None}) == {"a": None} + assert diff({"a": [1]}, {"a": {"x": 1}}) == {"a": {"x": 1}} + assert diff({"a": 1}, {"a": None}) == {"a": None} def test_new_dict_value_assigned_wholesale(): - assert compute_diff({}, {"a": {"x": 1}}) == {"a": {"x": 1}} + assert diff({}, {"a": {"x": 1}}) == {"a": {"x": 1}} def test_dollar_keys_escaped(): - assert compute_diff({}, {"$weird": 1}) == {"$$weird": 1} - assert compute_diff({"$weird": 1}, {"$weird": 2}) == {"$$weird": 2} - assert compute_diff({"$weird": 1}, {}) == {"$delete": ["$$weird"]} + assert diff({}, {"$weird": 1}) == {"$$weird": 1} + assert diff({"$weird": 1}, {"$weird": 2}) == {"$$weird": 2} + assert diff({"$weird": 1}, {}) == {"$delete": ["$$weird"]} def test_dollar_values_not_escaped(): # Only keys are escaped; values are stored verbatim, even "$delete". - assert compute_diff({"s": 1}, {"s": "$y"}) == {"s": "$y"} - assert compute_diff({"s": 1}, {"s": "$delete"}) == {"s": "$delete"} - assert compute_diff({}, {"o": {"s": "$y", "l": ["$z"]}}) == { + assert diff({"s": 1}, {"s": "$y"}) == {"s": "$y"} + assert diff({"s": 1}, {"s": "$delete"}) == {"s": "$delete"} + assert diff({}, {"o": {"s": "$y", "l": ["$z"]}}) == { "o": {"s": "$y", "l": ["$z"]} } -# --- Consumer: apply_diff / patch_state ------------------------------------- +# --- Consumer: apply_diff / patch ------------------------------------- -def test_patch_state_delegates(): - assert patch_state({"a": 1}, {"a": 2}) == {"a": 2} +def test_patch_delegates(): + assert patch({"a": 1}, {"a": 2}) == {"a": 2} def test_apply_scalar_and_add(): @@ -218,9 +218,9 @@ JSONDIFF_APPLIES_CASES = [ "name,old,new", JSONDIFF_APPLIES_CASES, ids=[c[0] for c in JSONDIFF_APPLIES_CASES] ) def test_jsondiff_applies_our_patches(name, old, new): - diff = compute_diff(old, new) - assert diff is not None - assert jsondiff.patch(old, diff, marshal=True) == new + delta = diff(old, new) + assert delta is not None + assert jsondiff.patch(old, delta, marshal=True) == new @pytest.mark.parametrize("name,old,new", COMPAT_CASES, ids=[c[0] for c in COMPAT_CASES]) @@ -231,15 +231,15 @@ def test_we_apply_jsondiff_patches(name, old, new): @pytest.mark.parametrize("name,old,new", COMPAT_CASES, ids=[c[0] for c in COMPAT_CASES]) def test_our_own_round_trip(name, old, new): - diff = compute_diff(old, new) - assert diff is not None - assert apply_diff(old, diff) == new + delta = diff(old, new) + assert delta is not None + assert apply_diff(old, delta) == new def test_no_diff_means_equal_states(): for _name, old, new in COMPAT_CASES: - assert compute_diff(old, new) is not None # cases really differ - assert compute_diff({"a": [1, {"b": "$x"}]}, {"a": [1, {"b": "$x"}]}) is None + assert diff(old, new) is not None # cases really differ + assert diff({"a": [1, {"b": "$x"}]}, {"a": [1, {"b": "$x"}]}) is None # --- Logging ----------------------------------------------------------------