Coverage for python/lsst/images/serialization/_migrations.py: 42%
22 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-30 04:21 -0700
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-30 04:21 -0700
1# This file is part of lsst-images.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
11"""Registry of per-schema migrations from one data-model major to the next.
13A migration is needed when a backward-incompatible version bump means the
14current model cannot validate an older tree directly: a renamed or retyped
15field, a split or merged field, a restructured sub-tree. Additive changes
16need no migration, because defaulting the new fields on input is enough.
18Registering one step per adjacent major pair means only adjacent transforms
19are ever written, and the reader chains them to cross a larger gap. See
20:ref:`lsst.images-schema-versioning` for how this pairs with
21``min_read_version``.
22"""
24from __future__ import annotations
26__all__ = ("migration",)
28from collections.abc import Callable
29from typing import Any, Protocol
32class Migration(Protocol):
33 """A function that advances an on-disk tree one data-model major."""
35 __qualname__: str
36 """Name used to identify this step in a duplicate-registration error."""
38 def __call__(self, data: dict[str, Any]) -> dict[str, Any]:
39 """Rewrite ``data`` into the next major's shape.
41 Parameters
42 ----------
43 data
44 Raw on-disk tree, at the major this migration was registered for.
45 May be mutated in place and returned; the caller has already
46 isolated it from any other union candidate.
48 Returns
49 -------
50 `dict`
51 The tree in the ``from_major + 1`` shape. Must not set
52 ``schema_version``; the caller does that as it chains steps.
53 """
54 ...
57_MIGRATIONS: dict[tuple[str, int], Migration] = {}
58"""Registered migrations, keyed by ``(schema name, from_major)``.
60Each entry advances a tree exactly one major.
61"""
63_MIGRATABLE_NAMES: set[str] = set()
64"""Schema names with at least one registered migration.
66The read path checks this set before doing any other work, so a schema with
67no migrations -- every schema in this package today -- pays only an empty-set
68truthiness test per validation.
69"""
72def migration(schema_name: str, from_major: int) -> Callable[[Migration], Migration]:
73 """Register a function that advances a tree one data-model major.
75 Parameters
76 ----------
77 schema_name
78 ``SCHEMA_NAME`` of the schema this migration applies to.
79 from_major
80 Major version the incoming tree is at. The function must return a
81 tree in the ``from_major + 1`` shape.
83 Returns
84 -------
85 `~collections.abc.Callable`
86 Decorator that registers and returns the function unchanged.
88 Raises
89 ------
90 RuntimeError
91 If a migration is already registered for this schema and major.
93 Notes
94 -----
95 The function receives the raw on-disk tree as a `dict` and may mutate and
96 return it. It must not set ``schema_version``; the caller does that as it
97 chains steps.
98 """
100 def register(func: Migration) -> Migration:
101 key = (schema_name, from_major)
102 if (existing := _MIGRATIONS.get(key)) is not None and existing is not func:
103 raise RuntimeError(
104 f"A migration for {schema_name!r} major {from_major} is already registered to "
105 f"{existing.__qualname__}; refusing to replace it with {func.__qualname__}."
106 )
107 _MIGRATIONS[key] = func
108 _MIGRATABLE_NAMES.add(schema_name)
109 return func
111 return register