Coverage for tests/test_serialization_registry.py: 96%
81 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 15:24 -0700
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 15:24 -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.
11from __future__ import annotations
13from typing import Any, ClassVar
14from unittest import mock
16import pytest
18from lsst.images import SkyProjection, VisitImage
19from lsst.images._image import ImageSerializationModel
20from lsst.images._visit_image import VisitImageSerializationModel
21from lsst.images.serialization import ArchiveTree, _io, class_for_schema, public_type_for_schema
22from lsst.images.serialization._io import _REGISTRY, register_schema_class
25def test_unknown_returns_none() -> None:
26 """Verify class_for_schema returns None for an unrecognised schema name."""
27 assert class_for_schema("does-not-exist") is None
30def test_visit_image_registered() -> None:
31 """Verify visit_image is registered and maps to
32 VisitImageSerializationModel.
33 """
34 cls = class_for_schema("visit_image")
35 assert cls is VisitImageSerializationModel
38def test_nested_image_registered() -> None:
39 """Verify nested types like image are registered so sub-models can
40 be read directly.
41 """
42 # Nested types are registered too -- "register all" was the spec
43 # decision so callers can read sub-models directly.
44 cls = class_for_schema("image")
45 assert cls is ImageSerializationModel
48def test_duplicate_registration_raises() -> None:
49 """Verify re-registering a class is a no-op but a conflicting class
50 raises RuntimeError.
51 """
52 # Re-registering the same class is a no-op.
53 register_schema_class(VisitImageSerializationModel)
55 # Defining a subclass that redeclares SCHEMA_NAME triggers
56 # __pydantic_init_subclass__, which calls register_schema_class
57 # with a different class object under an existing name. That
58 # call raises RuntimeError.
59 with pytest.raises(RuntimeError):
61 class _Imposter(VisitImageSerializationModel): # type: ignore[misc]
62 SCHEMA_NAME: ClassVar[str] = "visit_image"
63 SCHEMA_VERSION: ClassVar[str] = "1.0.0"
66def test_builtin_provider_loaded_on_miss() -> None:
67 """Verify builtin schema providers are loaded lazily on a registry
68 cache miss.
69 """
70 schema_names = ("cell_aperture_correction_map", "cell_coadd", "cell_psf", "coadd_provenance")
71 saved = {schema_name: _REGISTRY.pop(schema_name, None) for schema_name in schema_names}
72 try:
73 for schema_name in schema_names:
74 cls = class_for_schema(schema_name)
75 assert cls is not None, f"schema_name={schema_name!r}: expected a registered class"
76 assert cls.SCHEMA_NAME == schema_name, f"schema_name={schema_name!r}: SCHEMA_NAME mismatch"
77 finally:
78 # Preserve any registrations that existed before this test, even
79 # if an assertion above fails.
80 _REGISTRY.update({schema_name: cls for schema_name, cls in saved.items() if cls is not None})
83def test_entry_point_provider_loaded_on_miss() -> None:
84 """Verify entry-point providers are loaded lazily on a registry
85 cache miss.
86 """
88 class _EntryPointTree(ArchiveTree):
89 SCHEMA_NAME: ClassVar[str] = "_entry_point_schema_test"
90 SCHEMA_VERSION: ClassVar[str] = "1.0.0"
91 MIN_READ_VERSION: ClassVar[int] = 1
93 def deserialize(self, archive: Any, **kwargs: Any) -> VisitImage:
94 raise AssertionError("not used")
96 class _FakeEntryPoint:
97 value = "tests.test_serialization_registry:_EntryPointTree"
99 def load(self) -> type[ArchiveTree]:
100 return _EntryPointTree
102 _REGISTRY.pop("_entry_point_schema_test", None)
103 try:
104 with mock.patch.object(
105 _io.importlib.metadata,
106 "entry_points",
107 return_value=[_FakeEntryPoint()],
108 ) as entry_points:
109 cls = class_for_schema("_entry_point_schema_test")
110 entry_points.assert_called_once_with(
111 group="lsst.images.schemas",
112 name="_entry_point_schema_test",
113 )
114 assert cls is _EntryPointTree
115 finally:
116 _REGISTRY.pop("_entry_point_schema_test", None)
119def test_concrete_type() -> None:
120 """Verify public_type_for_schema returns VisitImage for the
121 visit_image schema.
122 """
123 assert public_type_for_schema("visit_image") is VisitImage
126def test_generic_in_memory_type() -> None:
127 """Verify public_type_for_schema returns SkyProjection for the
128 sky_projection schema.
129 """
130 # ProjectionSerializationModel produces a Projection (its PUBLIC_TYPE
131 # is the unparameterised class, not Projection[Any]).
132 assert public_type_for_schema("sky_projection") is SkyProjection
135def test_unregistered_schema_returns_none() -> None:
136 """Verify public_type_for_schema returns None for an unregistered
137 schema name.
138 """
139 assert public_type_for_schema("no-such-schema") is None
142def _all_concrete_archive_tree_subclasses() -> list[type]:
143 """Return all concrete ArchiveTree subclasses that declare SCHEMA_NAME."""
144 seen: list[type] = []
145 stack: list[type] = list(ArchiveTree.__subclasses__())
146 while stack:
147 cls = stack.pop()
148 stack.extend(cls.__subclasses__())
149 if "SCHEMA_NAME" in cls.__dict__:
150 seen.append(cls)
151 return seen
154def test_every_subclass_registered() -> None:
155 """Verify every package-local ArchiveTree subclass with SCHEMA_NAME
156 is registered.
157 """
158 # Test-local classes from other test methods may linger in
159 # __subclasses__ after their cleanup runs; restrict the check to
160 # classes whose modules belong to the package itself.
161 missing: list[str] = []
162 for cls in _all_concrete_archive_tree_subclasses():
163 if not cls.__module__.startswith("lsst.images"):
164 continue
165 registered = _REGISTRY.get(cls.SCHEMA_NAME)
166 if registered is None or registered is not cls: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 missing.append(f"{cls.__qualname__} -> {cls.SCHEMA_NAME}")
168 assert missing == [], f"Unregistered subclasses: {missing}"
171def test_every_registered_class_declares_public_type() -> None:
172 """Verify every package-local registered ArchiveTree class declares
173 PUBLIC_TYPE.
174 """
175 # Restrict to package-local classes; test-only ArchiveTree
176 # subclasses (e.g. tests/test_schema_versioning.py's
177 # _DummyArchiveTree) may register but are not part of the package.
178 unresolved: list[str] = []
179 for cls in _REGISTRY.values():
180 if not cls.__module__.startswith("lsst.images"):
181 continue
182 if not isinstance(getattr(cls, "PUBLIC_TYPE", None), type): 182 ↛ 183line 182 didn't jump to line 183 because the condition on line 182 was never true
183 unresolved.append(cls.__qualname__)
184 assert unresolved == [], f"No PUBLIC_TYPE declared: {unresolved}"