Coverage for python/lsst/images/_metadata.py: 48%

67 statements  

« 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 

12from __future__ import annotations 

13 

14__all__ = ("MetadataView",) 

15 

16from collections import ChainMap 

17from collections.abc import Iterator, Mapping, MutableMapping 

18from typing import Any, cast 

19 

20from .serialization import ExternalMetadata, ExternalMetadataValue, MetadataValue 

21 

22 

23class _NativeMetadata(MutableMapping[str, MetadataValue]): 

24 """The flexible metadata that is part of an image's data model. 

25 

26 Parameters 

27 ---------- 

28 data 

29 Dictionary holding the metadata. It is held by reference, so changes 

30 are visible to every object sharing it. 

31 external 

32 External metadata that new keys may not shadow. 

33 

34 Notes 

35 ----- 

36 Keys are case-sensitive. Adding a key that case-insensitively matches a 

37 key of ``external`` raises `KeyError`; a key that is already present may 

38 always be updated. 

39 """ 

40 

41 def __init__(self, data: dict[str, MetadataValue], external: ExternalMetadata) -> None: 

42 self._data = data 

43 self._external = external 

44 

45 def __getitem__(self, key: str) -> MetadataValue: 

46 return self._data[key] 

47 

48 def __setitem__(self, key: str, value: MetadataValue) -> None: 

49 if key not in self._data and key in self._external: 

50 raise KeyError( 

51 f"Metadata key {key!r} would shadow a key in the external metadata " 

52 "(external keys are case-insensitive)." 

53 ) 

54 self._data[key] = value 

55 

56 def __delitem__(self, key: str) -> None: 

57 del self._data[key] 

58 

59 def __iter__(self) -> Iterator[str]: 

60 return iter(self._data) 

61 

62 def __len__(self) -> int: 

63 return len(self._data) 

64 

65 def __repr__(self) -> str: 

66 return f"_NativeMetadata({self._data!r})" 

67 

68 

69class MetadataView(MutableMapping[str, MetadataValue | ExternalMetadataValue]): 

70 """Combined view of an image's native and external metadata. 

71 

72 Parameters 

73 ---------- 

74 native 

75 Metadata that is part of the image's data model. 

76 external 

77 Read-only metadata carried in from an external source. 

78 

79 Notes 

80 ----- 

81 Lookups check ``native`` first, matching case exactly, and then 

82 ``external``, ignoring case. Writes and deletes go to ``native`` only, 

83 and adding a new key that case-insensitively matches an ``external`` key 

84 raises `KeyError`. Iteration yields the exact-case union of the keys of 

85 both. 

86 

87 When a key is repeated in the external source, which of its values 

88 ``[]`` returns is unspecified; use `get_all` to obtain every value. 

89 """ 

90 

91 def __init__(self, native: _NativeMetadata, external: ExternalMetadata) -> None: 

92 self._native = native 

93 self._external = external 

94 # ChainMap only ever writes to its first mapping, so the read-only 

95 # external mapping is never mutated through it. 

96 self._chain = ChainMap( 

97 cast(MutableMapping[str, MetadataValue | ExternalMetadataValue], native), 

98 cast(MutableMapping[str, MetadataValue | ExternalMetadataValue], external), 

99 ) 

100 

101 @property 

102 def native(self) -> MutableMapping[str, MetadataValue]: 

103 """Metadata that is part of the image's data model and is saved with 

104 it (`~collections.abc.MutableMapping`). 

105 

106 Keys are case-sensitive. Adding a key that case-insensitively 

107 matches a key of `external` raises `KeyError`; a key that is already 

108 present may always be updated. 

109 """ 

110 return self._native 

111 

112 @property 

113 def external(self) -> ExternalMetadata: 

114 """Read-only metadata carried in from an external source, such as 

115 the primary header of a FITS file (`.serialization.ExternalMetadata`). 

116 """ 

117 return self._external 

118 

119 def __getitem__(self, key: str) -> MetadataValue | ExternalMetadataValue: 

120 return self._chain[key] 

121 

122 def __setitem__(self, key: str, value: MetadataValue | ExternalMetadataValue) -> None: 

123 self._chain[key] = value 

124 

125 def __delitem__(self, key: str) -> None: 

126 del self._chain[key] 

127 

128 def __contains__(self, key: object) -> bool: 

129 return key in self._chain 

130 

131 def __iter__(self) -> Iterator[str]: 

132 return iter(self._chain) 

133 

134 def __len__(self) -> int: 

135 return len(self._chain) 

136 

137 def __repr__(self) -> str: 

138 return f"MetadataView(native={dict(self._native)!r}, external={dict(self._external)!r})" 

139 

140 def clear(self) -> None: 

141 # The inherited implementation stops at the first external key. 

142 self._native.clear() 

143 

144 def popitem(self) -> tuple[str, MetadataValue]: 

145 # The inherited implementation may pick an external key. 

146 return self._native.popitem() 

147 

148 def get_all(self, key: str) -> tuple[MetadataValue | ExternalMetadataValue, ...]: 

149 """Return every value for a key. 

150 

151 Parameters 

152 ---------- 

153 key 

154 Key to look up. 

155 

156 Returns 

157 ------- 

158 values : `tuple` 

159 A single-element tuple holding the native value if ``key`` is a 

160 native key, and otherwise every external value for ``key``, in 

161 source order. 

162 

163 Raises 

164 ------ 

165 KeyError 

166 Raised if ``key`` is not present. 

167 """ 

168 if key in self._native: 

169 return (self._native[key],) 

170 return self._external.get_all(key) 

171 

172 def copy(self) -> dict[str, MetadataValue]: 

173 """Return a plain `dict` copy of the native metadata. 

174 

175 Returns 

176 ------- 

177 metadata : `dict` 

178 A copy of the native metadata. 

179 """ 

180 return dict(self._native) 

181 

182 __copy__ = copy 

183 

184 def __or__(self, other: Mapping[str, Any]) -> dict[str, Any]: 

185 return {**self, **other} 

186 

187 def __ror__(self, other: Mapping[str, Any]) -> dict[str, Any]: 

188 return {**other, **self} 

189 

190 def __ior__(self, other: Mapping[str, Any]) -> MetadataView: 

191 self.update(other) 

192 return self