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
« 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.
12from __future__ import annotations
14__all__ = ("MetadataView",)
16from collections import ChainMap
17from collections.abc import Iterator, Mapping, MutableMapping
18from typing import Any, cast
20from .serialization import ExternalMetadata, ExternalMetadataValue, MetadataValue
23class _NativeMetadata(MutableMapping[str, MetadataValue]):
24 """The flexible metadata that is part of an image's data model.
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.
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 """
41 def __init__(self, data: dict[str, MetadataValue], external: ExternalMetadata) -> None:
42 self._data = data
43 self._external = external
45 def __getitem__(self, key: str) -> MetadataValue:
46 return self._data[key]
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
56 def __delitem__(self, key: str) -> None:
57 del self._data[key]
59 def __iter__(self) -> Iterator[str]:
60 return iter(self._data)
62 def __len__(self) -> int:
63 return len(self._data)
65 def __repr__(self) -> str:
66 return f"_NativeMetadata({self._data!r})"
69class MetadataView(MutableMapping[str, MetadataValue | ExternalMetadataValue]):
70 """Combined view of an image's native and external metadata.
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.
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.
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 """
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 )
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`).
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
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
119 def __getitem__(self, key: str) -> MetadataValue | ExternalMetadataValue:
120 return self._chain[key]
122 def __setitem__(self, key: str, value: MetadataValue | ExternalMetadataValue) -> None:
123 self._chain[key] = value
125 def __delitem__(self, key: str) -> None:
126 del self._chain[key]
128 def __contains__(self, key: object) -> bool:
129 return key in self._chain
131 def __iter__(self) -> Iterator[str]:
132 return iter(self._chain)
134 def __len__(self) -> int:
135 return len(self._chain)
137 def __repr__(self) -> str:
138 return f"MetadataView(native={dict(self._native)!r}, external={dict(self._external)!r})"
140 def clear(self) -> None:
141 # The inherited implementation stops at the first external key.
142 self._native.clear()
144 def popitem(self) -> tuple[str, MetadataValue]:
145 # The inherited implementation may pick an external key.
146 return self._native.popitem()
148 def get_all(self, key: str) -> tuple[MetadataValue | ExternalMetadataValue, ...]:
149 """Return every value for a key.
151 Parameters
152 ----------
153 key
154 Key to look up.
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.
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)
172 def copy(self) -> dict[str, MetadataValue]:
173 """Return a plain `dict` copy of the native metadata.
175 Returns
176 -------
177 metadata : `dict`
178 A copy of the native metadata.
179 """
180 return dict(self._native)
182 __copy__ = copy
184 def __or__(self, other: Mapping[str, Any]) -> dict[str, Any]:
185 return {**self, **other}
187 def __ror__(self, other: Mapping[str, Any]) -> dict[str, Any]:
188 return {**other, **self}
190 def __ior__(self, other: Mapping[str, Any]) -> MetadataView:
191 self.update(other)
192 return self