Coverage for python/lsst/images/serialization/_migrations.py: 42%

22 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-16 09:42 +0000

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. 

12 

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. 

17 

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""" 

23 

24from __future__ import annotations 

25 

26__all__ = ("migration",) 

27 

28from collections.abc import Callable 

29from typing import Any, Protocol 

30 

31 

32class Migration(Protocol): 

33 """A function that advances an on-disk tree one data-model major.""" 

34 

35 __qualname__: str 

36 """Name used to identify this step in a duplicate-registration error.""" 

37 

38 def __call__(self, data: dict[str, Any]) -> dict[str, Any]: 

39 """Rewrite ``data`` into the next major's shape. 

40 

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. 

47 

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 ... 

55 

56 

57_MIGRATIONS: dict[tuple[str, int], Migration] = {} 

58"""Registered migrations, keyed by ``(schema name, from_major)``. 

59 

60Each entry advances a tree exactly one major. 

61""" 

62 

63_MIGRATABLE_NAMES: set[str] = set() 

64"""Schema names with at least one registered migration. 

65 

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""" 

70 

71 

72def migration(schema_name: str, from_major: int) -> Callable[[Migration], Migration]: 

73 """Register a function that advances a tree one data-model major. 

74 

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. 

82 

83 Returns 

84 ------- 

85 `~collections.abc.Callable` 

86 Decorator that registers and returns the function unchanged. 

87 

88 Raises 

89 ------ 

90 RuntimeError 

91 If a migration is already registered for this schema and major. 

92 

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 """ 

99 

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 

110 

111 return register