Coverage for python/lsst/images/serialization/_reader.py: 96%
60 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 09:14 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 09:14 +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"""User-facing ``open`` reader for incremental, component-wise reads."""
13from __future__ import annotations
15__all__ = ("Reader", "open")
17from collections.abc import Iterator
18from contextlib import AbstractContextManager, contextmanager
19from typing import Any, TypeVar, overload
21from lsst.resources import ResourcePathExpression
23from ._backends import backend_for_path
24from ._common import ArchiveTree, ButlerInfo, MetadataValue
25from ._input_archive import ArchiveInfo, InputArchive
26from ._io import public_type_for_schema
28# This pre-python-3.12 declaration is needed so Sphinx (the
29# autodoc-typehints plugin) can resolve the ``T`` forward reference in the
30# stringized annotations; the PEP 695 ``[T]`` parameters below are scoped to
31# their class/function and are not visible in the module globals.
32T = TypeVar("T")
35class Reader[T]:
36 """A handle to an open ``lsst.images`` file.
38 Returned by `open`.
39 Lets the caller pull individual components, or the whole object, out of a
40 file that is opened once; the underlying archive caches dereferenced
41 pointers so repeated reads share work.
42 Valid only inside the ``with`` block that produced it.
44 Parameters
45 ----------
46 archive
47 Input archive backing the open file.
48 tree
49 Validated on-disk deserialization tree for the file.
50 info
51 Schema name, version, URL, and format version for the file.
52 expected_cls
53 Expected type of the deserialized object, or `None` to accept any
54 type.
55 """
57 def __init__(
58 self,
59 archive: InputArchive[Any],
60 tree: ArchiveTree,
61 info: ArchiveInfo,
62 expected_cls: type[T] | None,
63 ) -> None:
64 self._archive = archive
65 self._tree = tree
66 self._info = info
67 self._expected_cls = expected_cls
68 self._closed = False
70 def _check_open(self) -> None:
71 if self._closed:
72 raise RuntimeError("Reader is closed; use it only inside its 'with' block.")
74 @property
75 def info(self) -> ArchiveInfo:
76 """Schema name/version/url and format version for this file."""
77 return self._info
79 @property
80 def metadata(self) -> dict[str, MetadataValue]:
81 """Flexible metadata stored with the object."""
82 return self._tree.metadata
84 @property
85 def butler_info(self) -> ButlerInfo | None:
86 """Butler dataset info stored with the object, or `None`."""
87 return self._tree.butler_info
89 def get_tree(self) -> ArchiveTree:
90 """Return the validated on-disk tree for advanced, low-level access.
92 Most callers want `read` or `get_component` instead; the tree is the
93 raw deserialization model that those methods build on.
94 """
95 self._check_open()
96 return self._tree
98 def get_component(self, name: str, **kwargs: Any) -> Any:
99 """Deserialize and return a single named component.
101 Raises `~lsst.images.serialization.InvalidComponentError` for an
102 unknown component name.
104 Parameters
105 ----------
106 name
107 Name of the component to deserialize.
108 **kwargs
109 Additional keyword arguments forwarded to the component
110 deserializer.
111 """
112 self._check_open()
113 return self._tree.deserialize_component(name, self._archive, **kwargs)
115 def read(self, **kwargs: Any) -> T:
116 """Deserialize and return the whole object.
118 Parameters
119 ----------
120 **kwargs
121 Additional keyword arguments forwarded to the deserializer.
122 """
123 self._check_open()
124 obj = self._tree.deserialize(self._archive, **kwargs)
125 if hasattr(obj, "_opaque_metadata"):
126 obj._opaque_metadata = self._archive.get_opaque_metadata()
127 if self._expected_cls is not None and not isinstance(obj, self._expected_cls): 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 raise TypeError(
129 f"{self._info.schema_name!r} deserialized to {type(obj).__name__}, "
130 f"not the requested {self._expected_cls.__name__}."
131 )
132 return obj
135@overload
136def open[T](
137 path: ResourcePathExpression, cls: type[T], *, partial: bool = ..., **backend_kwargs: Any
138) -> AbstractContextManager[Reader[T]]: ...
139@overload
140def open( 140 ↛ exitline 140 didn't return from function 'open' because
141 path: ResourcePathExpression, cls: None = ..., *, partial: bool = ..., **backend_kwargs: Any
142) -> AbstractContextManager[Reader[Any]]: ...
143@contextmanager
144def open(
145 path: ResourcePathExpression, cls: type[Any] | None = None, *, partial: bool = True, **backend_kwargs: Any
146) -> Iterator[Reader]:
147 """Open an ``lsst.images`` file for incremental, component-wise reads.
149 Dispatches to the appropriate backend by file extension, resolves the
150 registered in-memory type from the file's schema, and returns a `Reader`
151 context manager.
153 Parameters
154 ----------
155 path
156 File to read; convertible to `lsst.resources.ResourcePath`.
157 cls
158 Optional expected in-memory type.
159 When given, `open` validates that the file's schema resolves to a
160 subclass of ``cls`` (raising `TypeError` otherwise) and the returned
161 `Reader` is typed accordingly, so `Reader.read` needs no cast.
162 partial
163 Forwarded to the backend ``open_tree``; defaults to `True` (a reader
164 is for incremental access).
165 A no-op for the JSON and NDF backends.
166 **backend_kwargs
167 Backend-specific open options (e.g. ``page_size`` for FITS).
169 Raises
170 ------
171 ValueError
172 If the file extension is not recognized.
173 ArchiveReadError
174 If the file's schema is not registered.
175 TypeError
176 If ``cls`` is given and the file's schema resolves to an
177 incompatible type.
178 """
179 backend = backend_for_path(path)
180 with backend.input_archive.open_tree(path, partial=partial, **backend_kwargs) as (
181 archive,
182 tree,
183 info,
184 ):
185 if cls is not None:
186 resolved = public_type_for_schema(info.schema_name)
187 if resolved is not None and not issubclass(resolved, cls):
188 raise TypeError(
189 f"{path!r} has schema {info.schema_name!r} (type {resolved.__name__}), "
190 f"which is not a {cls.__name__}."
191 )
192 reader: Reader[Any] = Reader(archive, tree, info, cls)
193 try:
194 yield reader
195 finally:
196 reader._closed = True