Python · 2659 bytes Raw Blame History
1 """Migration dispatcher — walk a raw frontmatter dict up to target version.
2
3 Call path:
4
5 raw = yaml.safe_load(frontmatter_text)
6 migrated, applied = apply_pending(raw, target_version=CURRENT_SCHEMA_VERSION)
7 # `migrated` is now shape-compatible with the Pydantic model;
8 # `applied` is the list of from-versions that ran, e.g., [1, 2].
9
10 The dispatcher stamps `raw["dlm_version"] = v+1` after each successful
11 migrator so any migrator whose output is inspected mid-chain sees the
12 running version. Already-current (or newer) inputs return the original
13 raw with `applied=[]`.
14 """
15
16 from __future__ import annotations
17
18 from dlm.doc.errors import UnsupportedMigrationError
19 from dlm.doc.migrations import MIGRATORS
20
21
22 def apply_pending(
23 raw: dict[str, object], *, target_version: int
24 ) -> tuple[dict[str, object], list[int]]:
25 """Walk `raw` through the migrator chain up to `target_version`.
26
27 Returns `(migrated_raw, applied_from_versions)`. `applied` is the
28 list of `from_version` integers that ran, in order — empty when the
29 input was already at or above `target_version`.
30
31 Raises `UnsupportedMigrationError` when a `from_version` in the
32 chain has no migrator registered (the "manually-forked" case; the
33 coverage test catches missing migrators at commit time).
34 """
35 current = dict(raw)
36 applied: list[int] = []
37
38 while True:
39 version = current.get("dlm_version", 1)
40 if not isinstance(version, int) or isinstance(version, bool):
41 raise UnsupportedMigrationError(
42 f"dlm_version must be int, got {type(version).__name__}",
43 )
44 if version == target_version:
45 return current, applied
46 if version > target_version:
47 # Forward-dated document — refuse rather than
48 # silently reporting "already at target". The read-path
49 # (parse_file) already raises DlmVersionError on future
50 # versions; migrate should match that contract.
51 from dlm.doc.errors import DlmVersionError
52
53 raise DlmVersionError(
54 f"dlm_version {version} is newer than this parser "
55 f"(CURRENT_SCHEMA_VERSION={target_version}); upgrade dlm "
56 "or check the source's schema."
57 )
58 migrator = MIGRATORS.get(version)
59 if migrator is None:
60 raise UnsupportedMigrationError(
61 f"no migrator registered for dlm_version={version}; "
62 f"target_version={target_version}.",
63 )
64 current = migrator(current)
65 current["dlm_version"] = version + 1
66 applied.append(version)