Coverage for python/lsst/images/serialization/_reader.py: 96%

60 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-02 02:02 -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"""User-facing ``open`` reader for incremental, component-wise reads.""" 

12 

13from __future__ import annotations 

14 

15__all__ = ("Reader", "open") 

16 

17from collections.abc import Iterator 

18from contextlib import AbstractContextManager, contextmanager 

19from typing import Any, TypeVar, overload 

20 

21from lsst.resources import ResourcePathExpression 

22 

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 

27 

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

33 

34 

35class Reader[T]: 

36 """A handle to an open ``lsst.images`` file. 

37 

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. 

43 

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

56 

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 

69 

70 def _check_open(self) -> None: 

71 if self._closed: 

72 raise RuntimeError("Reader is closed; use it only inside its 'with' block.") 

73 

74 @property 

75 def info(self) -> ArchiveInfo: 

76 """Schema name/version/url and format version for this file.""" 

77 return self._info 

78 

79 @property 

80 def metadata(self) -> dict[str, MetadataValue]: 

81 """Flexible metadata stored with the object.""" 

82 return self._tree.metadata 

83 

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 

88 

89 def get_tree(self) -> ArchiveTree: 

90 """Return the validated on-disk tree for advanced, low-level access. 

91 

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 

97 

98 def get_component(self, name: str, **kwargs: Any) -> Any: 

99 """Deserialize and return a single named component. 

100 

101 Raises `~lsst.images.serialization.InvalidComponentError` for an 

102 unknown component name. 

103 

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) 

114 

115 def read(self, **kwargs: Any) -> T: 

116 """Deserialize and return the whole object. 

117 

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 

133 

134 

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. 

148 

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. 

152 

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

168 

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