Coverage for python/lsst/daf/butler/core/named.py : 40%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21from __future__ import annotations
23__all__ = (
24 "IndexedTupleDict",
25 "NamedKeyDict",
26 "NamedKeyMapping",
27 "NamedValueSet",
28)
30from abc import abstractmethod
31from typing import (
32 AbstractSet,
33 Any,
34 Dict,
35 ItemsView,
36 Iterable,
37 Iterator,
38 KeysView,
39 Mapping,
40 MutableMapping,
41 MutableSet,
42 Tuple,
43 TypeVar,
44 Union,
45 ValuesView,
46)
47from types import MappingProxyType
48try:
49 # If we're running mypy, we should have typing_extensions.
50 # If we aren't running mypy, we shouldn't assume we do.
51 # When we're safely on Python 3.8, we can import Protocol
52 # from typing and avoid all of this.
53 from typing_extensions import Protocol
55 class Named(Protocol):
56 @property
57 def name(self) -> str:
58 pass
60except ImportError:
61 Named = Any # type: ignore
64K = TypeVar("K", bound=Named)
65V = TypeVar("V")
68class NamedKeyMapping(Mapping[K, V]):
69 """An abstract base class for custom mappings whose keys are objects with
70 a `str` ``name`` attribute, for which lookups on the name as well as the
71 object are permitted.
73 Notes
74 -----
75 In addition to the new `names` property and `byName` method, this class
76 simply redefines the type signature for `__getitem__` and `get` that would
77 otherwise be inherited from `Mapping`. That is only relevant for static
78 type checking; the actual Python runtime doesn't care about types at all.
79 """
81 @property
82 @abstractmethod
83 def names(self) -> AbstractSet[str]:
84 """The set of names associated with the keys, in the same order
85 (`AbstractSet` [ `str` ]).
86 """
87 raise NotImplementedError()
89 @abstractmethod
90 def byName(self) -> Dict[str, V]:
91 """Return a `Mapping` with names as keys and the same values as
92 ``self``.
94 Returns
95 -------
96 dictionary : `dict`
97 A dictionary with the same values (and iteration order) as
98 ``self``, with `str` names as keys. This is always a new object,
99 not a view.
100 """
101 raise NotImplementedError()
103 @abstractmethod
104 def __getitem__(self, key: Union[str, K]) -> V:
105 raise NotImplementedError()
107 def get(self, key: Union[str, K], default: Any = None) -> Any:
108 # Delegating to super is not allowed by typing, because it doesn't
109 # accept str, but we know it just delegates to __getitem__, which does.
110 return super().get(key, default) # type: ignore
113class NamedKeyMutableMapping(NamedKeyMapping[K, V], MutableMapping[K, V]):
114 """An abstract base class that adds mutation to `NamedKeyMapping`.
115 """
117 @abstractmethod
118 def __setitem__(self, key: Union[str, K], value: V) -> None:
119 raise NotImplementedError()
121 @abstractmethod
122 def __delitem__(self, key: Union[str, K]) -> None:
123 raise NotImplementedError()
125 def pop(self, key: Union[str, K], default: Any = None) -> Any:
126 # See comment in `NamedKeyMapping.get`; same logic applies here.
127 return super().pop(key, default) # type: ignore
130class NamedKeyDict(NamedKeyMutableMapping[K, V]):
131 """A dictionary wrapper that require keys to have a ``.name`` attribute,
132 and permits lookups using either key objects or their names.
134 Names can be used in place of keys when updating existing items, but not
135 when adding new items.
137 It is assumed (but asserted) that all name equality is equivalent to key
138 equality, either because the key objects define equality this way, or
139 because different objects with the same name are never included in the same
140 dictionary.
142 Parameters
143 ----------
144 args
145 All positional constructor arguments are forwarded directly to `dict`.
146 Keyword arguments are not accepted, because plain strings are not valid
147 keys for `NamedKeyDict`.
149 Raises
150 ------
151 AttributeError
152 Raised when an attempt is made to add an object with no ``.name``
153 attribute to the dictionary.
154 AssertionError
155 Raised when multiple keys have the same name.
156 """
158 __slots__ = ("_dict", "_names",)
160 def __init__(self, *args: Any):
161 self._dict: Dict[K, V] = dict(*args)
162 self._names = {key.name: key for key in self._dict}
163 assert len(self._names) == len(self._dict), "Duplicate names in keys."
165 @property
166 def names(self) -> KeysView[str]:
167 """The set of names associated with the keys, in the same order
168 (`~collections.abc.KeysView`).
169 """
170 return self._names.keys()
172 def byName(self) -> Dict[str, V]:
173 """Return a `dict` with names as keys and the same values as ``self``.
174 """
175 return dict(zip(self._names.keys(), self._dict.values()))
177 def __len__(self) -> int:
178 return len(self._dict)
180 def __iter__(self) -> Iterator[K]:
181 return iter(self._dict)
183 def __str__(self) -> str:
184 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items()))
186 def __repr__(self) -> str:
187 return "NamedKeyDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items()))
189 def __getitem__(self, key: Union[str, K]) -> V:
190 if isinstance(key, str):
191 return self._dict[self._names[key]]
192 else:
193 return self._dict[key]
195 def __setitem__(self, key: Union[str, K], value: V) -> None:
196 if isinstance(key, str):
197 self._dict[self._names[key]] = value
198 else:
199 assert self._names.get(key.name, key) == key, "Name is already associated with a different key."
200 self._dict[key] = value
201 self._names[key.name] = key
203 def __delitem__(self, key: Union[str, K]) -> None:
204 if isinstance(key, str):
205 del self._dict[self._names[key]]
206 del self._names[key]
207 else:
208 del self._dict[key]
209 del self._names[key.name]
211 def keys(self) -> KeysView[K]:
212 return self._dict.keys()
214 def values(self) -> ValuesView[V]:
215 return self._dict.values()
217 def items(self) -> ItemsView[K, V]:
218 return self._dict.items()
220 def copy(self) -> NamedKeyDict[K, V]:
221 result = NamedKeyDict.__new__(NamedKeyDict)
222 result._dict = dict(self._dict)
223 result._names = dict(self._names)
224 return result
226 def freeze(self) -> None:
227 """Disable all mutators, effectively transforming ``self`` into
228 an immutable mapping.
229 """
230 if not isinstance(self._dict, MappingProxyType):
231 self._dict = MappingProxyType(self._dict) # type: ignore
234class NamedValueSet(MutableSet[K]):
235 """A custom mutable set class that requires elements to have a ``.name``
236 attribute, which can then be used as keys in `dict`-like lookup.
238 Names and elements can both be used with the ``in`` and ``del``
239 operators, `remove`, and `discard`. Names (but not elements)
240 can be used with ``[]``-based element retrieval (not assignment)
241 and the `get` method. `pop` can be used in either its `MutableSet`
242 form (no arguments; an arbitrary element is returned) or its
243 `MutableMapping` form (one or two arguments for the name and
244 optional default value, respectively).
246 Parameters
247 ----------
248 elements : `iterable`
249 Iterable over elements to include in the set.
251 Raises
252 ------
253 AttributeError
254 Raised if one or more elements do not have a ``.name`` attribute.
256 Notes
257 -----
258 Iteration order is guaranteed to be the same as insertion order (with
259 the same general behavior as `dict` ordering).
260 Like `dicts`, sets with the same elements will compare as equal even if
261 their iterator order is not the same.
262 """
264 __slots__ = ("_dict",)
266 def __init__(self, elements: Iterable[K] = ()):
267 self._dict = {element.name: element for element in elements}
269 @property
270 def names(self) -> KeysView[str]:
271 """The set of element names, in the same order
272 (`~collections.abc.KeysView`).
273 """
274 return self._dict.keys()
276 def asDict(self) -> Mapping[str, K]:
277 """Return a mapping view with names as keys.
279 Returns
280 -------
281 dict : `Mapping`
282 A dictionary-like view with ``values() == self``.
283 """
284 return self._dict
286 def __contains__(self, key: Any) -> bool:
287 return getattr(key, "name", key) in self._dict
289 def __len__(self) -> int:
290 return len(self._dict)
292 def __iter__(self) -> Iterator[K]:
293 return iter(self._dict.values())
295 def __str__(self) -> str:
296 return "{{{}}}".format(", ".join(str(element) for element in self))
298 def __repr__(self) -> str:
299 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self))
301 def __eq__(self, other: Any) -> Union[bool, NotImplemented]:
302 if isinstance(other, NamedValueSet):
303 return self._dict.keys() == other._dict.keys()
304 else:
305 return NotImplemented
307 def __hash__(self) -> int:
308 return hash(frozenset(self._dict.keys()))
310 # As per Set's docs, overriding just __le__ and __ge__ for performance will
311 # cover the other comparisons, too.
313 def __le__(self, other: AbstractSet[K]) -> Union[bool, NotImplemented]:
314 if isinstance(other, NamedValueSet):
315 return self._dict.keys() <= other._dict.keys()
316 else:
317 return NotImplemented
319 def __ge__(self, other: AbstractSet[K]) -> Union[bool, NotImplemented]:
320 if isinstance(other, NamedValueSet):
321 return self._dict.keys() >= other._dict.keys()
322 else:
323 return NotImplemented
325 def issubset(self, other: AbstractSet[K]) -> bool:
326 return self <= other
328 def issuperset(self, other: AbstractSet[K]) -> bool:
329 return self >= other
331 def __getitem__(self, name: str) -> K:
332 return self._dict[name]
334 def get(self, name: str, default: Any = None) -> Any:
335 """Return the element with the given name, or ``default`` if
336 no such element is present.
337 """
338 return self._dict.get(name, default)
340 def __delitem__(self, name: str) -> None:
341 del self._dict[name]
343 def add(self, element: K) -> None:
344 """Add an element to the set.
346 Raises
347 ------
348 AttributeError
349 Raised if the element does not have a ``.name`` attribute.
350 """
351 self._dict[element.name] = element
353 def remove(self, element: Union[str, K]) -> Any:
354 """Remove an element from the set.
356 Parameters
357 ----------
358 element : `object` or `str`
359 Element to remove or the string name thereof. Assumed to be an
360 element if it has a ``.name`` attribute.
362 Raises
363 ------
364 KeyError
365 Raised if an element with the given name does not exist.
366 """
367 del self._dict[getattr(element, "name", element)]
369 def discard(self, element: Union[str, K]) -> Any:
370 """Remove an element from the set if it exists.
372 Does nothing if no matching element is present.
374 Parameters
375 ----------
376 element : `object` or `str`
377 Element to remove or the string name thereof. Assumed to be an
378 element if it has a ``.name`` attribute.
379 """
380 try:
381 self.remove(element)
382 except KeyError:
383 pass
385 def pop(self, *args: str) -> K:
386 """Remove and return an element from the set.
388 Parameters
389 ----------
390 name : `str`, optional
391 Name of the element to remove and return. Must be passed
392 positionally. If not provided, an arbitrary element is
393 removed and returned.
395 Raises
396 ------
397 KeyError
398 Raised if ``name`` is provided but ``default`` is not, and no
399 matching element exists.
400 """
401 if not args:
402 return super().pop()
403 else:
404 return self._dict.pop(*args)
406 def copy(self) -> NamedValueSet[K]:
407 result = NamedValueSet.__new__(NamedValueSet)
408 result._dict = dict(self._dict)
409 return result
411 def freeze(self) -> None:
412 """Disable all mutators, effectively transforming ``self`` into
413 an immutable set.
414 """
415 if not isinstance(self._dict, MappingProxyType):
416 self._dict = MappingProxyType(self._dict) # type: ignore
419class IndexedTupleDict(NamedKeyMapping[K, V]):
420 """An immutable mapping that combines a tuple of values with a (possibly
421 shared) mapping from key to tuple index.
423 Parameters
424 ----------
425 indices: `NamedKeyDict`
426 Mapping from key to integer index in the values tuple. This mapping
427 is used as-is, not copied or converted to a true `dict`, which means
428 that the caller must guarantee that it will not be modified by other
429 (shared) owners in the future.
430 The caller is also responsible for guaranteeing that the indices in
431 the mapping are all valid for the given tuple.
432 values: `tuple`
433 Tuple of values for the dictionary. This may have a length greater
434 than the length of indices; these values are not considered part of
435 the mapping.
436 """
438 __slots__ = ("_indices", "_values")
440 def __init__(self, indices: NamedKeyDict[K, int], values: Tuple[V, ...]):
441 assert tuple(indices.values()) == tuple(range(len(values)))
442 self._indices = indices
443 self._values = values
445 @property
446 def names(self) -> KeysView[str]:
447 return self._indices.names
449 def byName(self) -> Dict[str, V]:
450 return dict(zip(self.names, self._values))
452 def __getitem__(self, key: Union[str, K]) -> V:
453 return self._values[self._indices[key]]
455 def __iter__(self) -> Iterator[K]:
456 return iter(self._indices)
458 def __len__(self) -> int:
459 return len(self._indices)
461 def __str__(self) -> str:
462 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items()))
464 def __repr__(self) -> str:
465 return "IndexedTupleDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items()))
467 def __contains__(self, key: Any) -> bool:
468 return key in self._indices
470 def keys(self) -> KeysView[K]:
471 return self._indices.keys()
473 # Tuple meets all requirements of ValuesView, but the Python typing system
474 # doesn't recognize it as substitutable, perhaps because it only really is
475 # for immutable mappings where there's no need to worry about the view
476 # being updated because the mapping changed.
477 def values(self) -> Tuple[V, ...]: # type: ignore
478 return self._values
480 # Let Mapping base class provide items(); we can't do it any more
481 # efficiently ourselves.
483 # These private attributes need to have types annotated outside __new__
484 # because mypy hasn't learned (yet) how to infer instance attribute types
485 # there they way it can with __init__.
486 _indices: NamedKeyDict[K, int]
487 _values: Tuple[V, ...]