Coverage for python/lsst/daf/butler/core/named.py: 47%
192 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-02 09:50 +0000
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-02 09:50 +0000
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 "NamedKeyDict",
25 "NamedKeyMapping",
26 "NamedValueAbstractSet",
27 "NamedValueMutableSet",
28 "NamedValueSet",
29 "NameLookupMapping",
30 "NameMappingSetView",
31)
33from abc import abstractmethod
34from types import MappingProxyType
35from typing import (
36 AbstractSet,
37 Any,
38 Dict,
39 ItemsView,
40 Iterable,
41 Iterator,
42 KeysView,
43 Mapping,
44 MutableMapping,
45 MutableSet,
46 Protocol,
47 TypeVar,
48 Union,
49 ValuesView,
50)
53class Named(Protocol):
54 """Protocol for objects with string name.
56 A non-inheritance interface for objects that have a string name that
57 maps directly to their equality comparisons.
58 """
60 @property
61 def name(self) -> str:
62 pass
65K = TypeVar("K", bound=Named)
66K_co = TypeVar("K_co", bound=Named, covariant=True)
67V = TypeVar("V")
68V_co = TypeVar("V_co", covariant=True)
71class NamedKeyMapping(Mapping[K, V_co]):
72 """Custom mapping class.
74 An abstract base class for custom mappings whose keys are objects with
75 a `str` ``name`` attribute, for which lookups on the name as well as the
76 object are permitted.
78 Notes
79 -----
80 In addition to the new `names` property and `byName` method, this class
81 simply redefines the type signature for `__getitem__` and `get` that would
82 otherwise be inherited from `Mapping`. That is only relevant for static
83 type checking; the actual Python runtime doesn't care about types at all.
84 """
86 __slots__ = ()
88 @property
89 @abstractmethod
90 def names(self) -> AbstractSet[str]:
91 """Return the set of names associated with the keys, in the same order.
93 (`AbstractSet` [ `str` ]).
94 """
95 raise NotImplementedError()
97 def byName(self) -> Dict[str, V_co]:
98 """Return a `Mapping` with names as keys and the ``self`` values.
100 Returns
101 -------
102 dictionary : `dict`
103 A dictionary with the same values (and iteration order) as
104 ``self``, with `str` names as keys. This is always a new object,
105 not a view.
106 """
107 return dict(zip(self.names, self.values()))
109 @abstractmethod
110 def keys(self) -> NamedValueAbstractSet[K]: # type: ignore
111 # TODO: docs
112 raise NotImplementedError()
114 @abstractmethod
115 def __getitem__(self, key: Union[str, K]) -> V_co:
116 raise NotImplementedError()
118 def get(self, key: Union[str, K], default: Any = None) -> Any:
119 # Delegating to super is not allowed by typing, because it doesn't
120 # accept str, but we know it just delegates to __getitem__, which does.
121 return super().get(key, default) # type: ignore
124NameLookupMapping = Union[NamedKeyMapping[K, V_co], Mapping[str, V_co]]
125"""A type annotation alias for signatures that want to use ``mapping[s]``
126(or ``mapping.get(s)``) where ``s`` is a `str`, and don't care whether
127``mapping.keys()`` returns named objects or direct `str` instances.
128"""
131class NamedKeyMutableMapping(NamedKeyMapping[K, V], MutableMapping[K, V]):
132 """An abstract base class that adds mutation to `NamedKeyMapping`."""
134 __slots__ = ()
136 @abstractmethod
137 def __setitem__(self, key: Union[str, K], value: V) -> None:
138 raise NotImplementedError()
140 @abstractmethod
141 def __delitem__(self, key: Union[str, K]) -> None:
142 raise NotImplementedError()
144 def pop(self, key: Union[str, K], default: Any = None) -> Any:
145 # See comment in `NamedKeyMapping.get`; same logic applies here.
146 return super().pop(key, default) # type: ignore
149class NamedKeyDict(NamedKeyMutableMapping[K, V]):
150 """Dictionary wrapper for named keys.
152 Requires keys to have a ``.name`` attribute,
153 and permits lookups using either key objects or their names.
155 Names can be used in place of keys when updating existing items, but not
156 when adding new items.
158 It is assumed (but asserted) that all name equality is equivalent to key
159 equality, either because the key objects define equality this way, or
160 because different objects with the same name are never included in the same
161 dictionary.
163 Parameters
164 ----------
165 args
166 All positional constructor arguments are forwarded directly to `dict`.
167 Keyword arguments are not accepted, because plain strings are not valid
168 keys for `NamedKeyDict`.
170 Raises
171 ------
172 AttributeError
173 Raised when an attempt is made to add an object with no ``.name``
174 attribute to the dictionary.
175 AssertionError
176 Raised when multiple keys have the same name.
177 """
179 __slots__ = (
180 "_dict",
181 "_names",
182 )
184 def __init__(self, *args: Any):
185 self._dict: Dict[K, V] = dict(*args)
186 self._names = {key.name: key for key in self._dict}
187 assert len(self._names) == len(self._dict), "Duplicate names in keys."
189 @property
190 def names(self) -> KeysView[str]:
191 """Return set of names associated with the keys, in the same order.
193 (`~collections.abc.KeysView`).
194 """
195 return self._names.keys()
197 def byName(self) -> Dict[str, V]:
198 """Return a `dict` with names as keys and the ``self`` values."""
199 return dict(zip(self._names.keys(), self._dict.values()))
201 def __len__(self) -> int:
202 return len(self._dict)
204 def __iter__(self) -> Iterator[K]:
205 return iter(self._dict)
207 def __str__(self) -> str:
208 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items()))
210 def __repr__(self) -> str:
211 return "NamedKeyDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items()))
213 def __getitem__(self, key: Union[str, K]) -> V:
214 if isinstance(key, str):
215 return self._dict[self._names[key]]
216 else:
217 return self._dict[key]
219 def __setitem__(self, key: Union[str, K], value: V) -> None:
220 if isinstance(key, str):
221 self._dict[self._names[key]] = value
222 else:
223 assert self._names.get(key.name, key) == key, "Name is already associated with a different key."
224 self._dict[key] = value
225 self._names[key.name] = key
227 def __delitem__(self, key: Union[str, K]) -> None:
228 if isinstance(key, str):
229 del self._dict[self._names[key]]
230 del self._names[key]
231 else:
232 del self._dict[key]
233 del self._names[key.name]
235 def keys(self) -> NamedValueAbstractSet[K]: # type: ignore
236 return NameMappingSetView(self._names)
238 def values(self) -> ValuesView[V]:
239 return self._dict.values()
241 def items(self) -> ItemsView[K, V]:
242 return self._dict.items()
244 def copy(self) -> NamedKeyDict[K, V]:
245 """Return a new `NamedKeyDict` with the same elements."""
246 result = NamedKeyDict.__new__(NamedKeyDict)
247 result._dict = dict(self._dict)
248 result._names = dict(self._names)
249 return result
251 def freeze(self) -> NamedKeyMapping[K, V]:
252 """Disable all mutators.
254 Effectively transforms ``self`` into an immutable mapping.
256 Returns
257 -------
258 self : `NamedKeyMapping`
259 While ``self`` is modified in-place, it is also returned with a
260 type annotation that reflects its new, frozen state; assigning it
261 to a new variable (and considering any previous references
262 invalidated) should allow for more accurate static type checking.
263 """
264 if not isinstance(self._dict, MappingProxyType):
265 self._dict = MappingProxyType(self._dict) # type: ignore
266 return self
269class NamedValueAbstractSet(AbstractSet[K_co]):
270 """Custom sets with named elements.
272 An abstract base class for custom sets whose elements are objects with
273 a `str` ``name`` attribute, allowing some dict-like operations and
274 views to be supported.
275 """
277 __slots__ = ()
279 @property
280 @abstractmethod
281 def names(self) -> AbstractSet[str]:
282 """Return set of names associated with the keys, in the same order.
284 (`AbstractSet` [ `str` ]).
285 """
286 raise NotImplementedError()
288 @abstractmethod
289 def asMapping(self) -> Mapping[str, K_co]:
290 """Return a mapping view with names as keys.
292 Returns
293 -------
294 dict : `Mapping`
295 A dictionary-like view with ``values() == self``.
296 """
297 raise NotImplementedError()
299 @abstractmethod
300 def __getitem__(self, key: Union[str, K_co]) -> K_co:
301 raise NotImplementedError()
303 def get(self, key: Union[str, K_co], default: Any = None) -> Any:
304 """Return the element with the given name.
306 Returns ``default`` if no such element is present.
307 """
308 try:
309 return self[key]
310 except KeyError:
311 return default
313 @classmethod
314 def _from_iterable(cls, iterable: Iterable[K_co]) -> NamedValueSet[K_co]:
315 """Construct class from an iterable.
317 Hook to ensure that inherited `collections.abc.Set` operators return
318 `NamedValueSet` instances, not something else (see `collections.abc`
319 documentation for more information).
321 Note that this behavior can only be guaranteed when both operands are
322 `NamedValueAbstractSet` instances.
323 """
324 return NamedValueSet(iterable)
327class NameMappingSetView(NamedValueAbstractSet[K_co]):
328 """A lightweight implementation of `NamedValueAbstractSet`.
330 Backed by a mapping from name to named object.
332 Parameters
333 ----------
334 mapping : `Mapping` [ `str`, `object` ]
335 Mapping this object will provide a view of.
336 """
338 def __init__(self, mapping: Mapping[str, K_co]):
339 self._mapping = mapping
341 __slots__ = ("_mapping",)
343 @property
344 def names(self) -> AbstractSet[str]:
345 # Docstring inherited from NamedValueAbstractSet.
346 return self._mapping.keys()
348 def asMapping(self) -> Mapping[str, K_co]:
349 # Docstring inherited from NamedValueAbstractSet.
350 return self._mapping
352 def __getitem__(self, key: Union[str, K_co]) -> K_co:
353 if isinstance(key, str):
354 return self._mapping[key]
355 else:
356 return self._mapping[key.name]
358 def __contains__(self, key: Any) -> bool:
359 return getattr(key, "name", key) in self._mapping
361 def __len__(self) -> int:
362 return len(self._mapping)
364 def __iter__(self) -> Iterator[K_co]:
365 return iter(self._mapping.values())
367 def __eq__(self, other: Any) -> bool:
368 if isinstance(other, NamedValueAbstractSet):
369 return self.names == other.names
370 else:
371 return set(self._mapping.values()) == other
373 def __le__(self, other: AbstractSet[K]) -> bool:
374 if isinstance(other, NamedValueAbstractSet):
375 return self.names <= other.names
376 else:
377 return set(self._mapping.values()) <= other
379 def __ge__(self, other: AbstractSet[K]) -> bool:
380 if isinstance(other, NamedValueAbstractSet):
381 return self.names >= other.names
382 else:
383 return set(self._mapping.values()) >= other
385 def __str__(self) -> str:
386 return "{{{}}}".format(", ".join(str(element) for element in self))
388 def __repr__(self) -> str:
389 return f"NameMappingSetView({self._mapping})"
392class NamedValueMutableSet(NamedValueAbstractSet[K], MutableSet[K]):
393 """Mutable variant of `NamedValueAbstractSet`.
395 Methods that can add new elements to the set are unchanged from their
396 `MutableSet` definitions, while those that only remove them can generally
397 accept names or element instances. `pop` can be used in either its
398 `MutableSet` form (no arguments; an arbitrary element is returned) or its
399 `MutableMapping` form (one or two arguments for the name and optional
400 default value, respectively). A `MutableMapping`-like `__delitem__`
401 interface is also included, which takes only names (like
402 `NamedValueAbstractSet.__getitem__`).
403 """
405 __slots__ = ()
407 @abstractmethod
408 def __delitem__(self, name: str) -> None:
409 raise NotImplementedError()
411 @abstractmethod
412 def remove(self, element: Union[str, K]) -> Any:
413 """Remove an element from the set.
415 Parameters
416 ----------
417 element : `object` or `str`
418 Element to remove or the string name thereof. Assumed to be an
419 element if it has a ``.name`` attribute.
421 Raises
422 ------
423 KeyError
424 Raised if an element with the given name does not exist.
425 """
426 raise NotImplementedError()
428 @abstractmethod
429 def discard(self, element: Union[str, K]) -> Any:
430 """Remove an element from the set if it exists.
432 Does nothing if no matching element is present.
434 Parameters
435 ----------
436 element : `object` or `str`
437 Element to remove or the string name thereof. Assumed to be an
438 element if it has a ``.name`` attribute.
439 """
440 raise NotImplementedError()
442 @abstractmethod
443 def pop(self, *args: str) -> K:
444 """Remove and return an element from the set.
446 Parameters
447 ----------
448 name : `str`, optional
449 Name of the element to remove and return. Must be passed
450 positionally. If not provided, an arbitrary element is
451 removed and returned.
453 Raises
454 ------
455 KeyError
456 Raised if ``name`` is provided but ``default`` is not, and no
457 matching element exists.
458 """
459 raise NotImplementedError()
462class NamedValueSet(NameMappingSetView[K], NamedValueMutableSet[K]):
463 """Custom mutable set class.
465 A custom mutable set class that requires elements to have a ``.name``
466 attribute, which can then be used as keys in `dict`-like lookup.
468 Names and elements can both be used with the ``in`` and ``del``
469 operators, `remove`, and `discard`. Names (but not elements)
470 can be used with ``[]``-based element retrieval (not assignment)
471 and the `get` method.
473 Parameters
474 ----------
475 elements : `iterable`
476 Iterable over elements to include in the set.
478 Raises
479 ------
480 AttributeError
481 Raised if one or more elements do not have a ``.name`` attribute.
483 Notes
484 -----
485 Iteration order is guaranteed to be the same as insertion order (with
486 the same general behavior as `dict` ordering).
487 Like `dicts`, sets with the same elements will compare as equal even if
488 their iterator order is not the same.
489 """
491 def __init__(self, elements: Iterable[K] = ()):
492 super().__init__({element.name: element for element in elements})
494 def __repr__(self) -> str:
495 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self))
497 def issubset(self, other: AbstractSet[K]) -> bool:
498 return self <= other
500 def issuperset(self, other: AbstractSet[K]) -> bool:
501 return self >= other
503 def __delitem__(self, name: str) -> None:
504 del self._mapping[name]
506 def add(self, element: K) -> None:
507 """Add an element to the set.
509 Raises
510 ------
511 AttributeError
512 Raised if the element does not have a ``.name`` attribute.
513 """
514 self._mapping[element.name] = element
516 def clear(self) -> None:
517 # Docstring inherited.
518 self._mapping.clear()
520 def remove(self, element: Union[str, K]) -> Any:
521 # Docstring inherited.
522 k = getattr(element, "name") if not isinstance(element, str) else element
523 del self._mapping[k]
525 def discard(self, element: Union[str, K]) -> Any:
526 # Docstring inherited.
527 try:
528 self.remove(element)
529 except KeyError:
530 pass
532 def pop(self, *args: str) -> K:
533 # Docstring inherited.
534 if not args:
535 # Parent is abstract method and we cannot call MutableSet
536 # implementation directly. Instead follow MutableSet and
537 # choose first element from iteration.
538 it = iter(self._mapping)
539 try:
540 value = next(it)
541 except StopIteration:
542 raise KeyError from None
543 args = (value,)
545 return self._mapping.pop(*args)
547 def update(self, elements: Iterable[K]) -> None:
548 """Add multiple new elements to the set.
550 Parameters
551 ----------
552 elements : `Iterable`
553 Elements to add.
554 """
555 for element in elements:
556 self.add(element)
558 def copy(self) -> NamedValueSet[K]:
559 """Return a new `NamedValueSet` with the same elements."""
560 result = NamedValueSet.__new__(NamedValueSet)
561 result._mapping = dict(self._mapping)
562 return result
564 def freeze(self) -> NamedValueAbstractSet[K]:
565 """Disable all mutators.
567 Effectively transforming ``self`` into an immutable set.
569 Returns
570 -------
571 self : `NamedValueAbstractSet`
572 While ``self`` is modified in-place, it is also returned with a
573 type annotation that reflects its new, frozen state; assigning it
574 to a new variable (and considering any previous references
575 invalidated) should allow for more accurate static type checking.
576 """
577 if not isinstance(self._mapping, MappingProxyType):
578 self._mapping = MappingProxyType(self._mapping) # type: ignore
579 return self
581 _mapping: Dict[str, K]