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

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 "NamedKeyDict",
25 "NamedKeyMapping",
26 "NamedValueAbstractSet",
27 "NamedValueMutableSet",
28 "NamedValueSet",
29 "NameLookupMapping",
30 "NameMappingSetView",
31)
33from abc import abstractmethod
34from typing import (
35 AbstractSet,
36 Any,
37 Dict,
38 ItemsView,
39 Iterable,
40 Iterator,
41 KeysView,
42 Mapping,
43 MutableMapping,
44 MutableSet,
45 TypeVar,
46 Union,
47 ValuesView,
48)
49from types import MappingProxyType
50try:
51 # If we're running mypy, we should have typing_extensions.
52 # If we aren't running mypy, we shouldn't assume we do.
53 # When we're safely on Python 3.8, we can import Protocol
54 # from typing and avoid all of this.
55 from typing_extensions import Protocol
57 class Named(Protocol):
58 @property
59 def name(self) -> str:
60 pass
62except ImportError:
63 Named = Any # type: ignore
66K = TypeVar("K", bound=Named)
67K_co = TypeVar("K_co", bound=Named, covariant=True)
68V = TypeVar("V")
69V_co = TypeVar("V_co", covariant=True)
72class NamedKeyMapping(Mapping[K_co, V_co]):
73 """An abstract base class for custom mappings whose keys are objects with
74 a `str` ``name`` attribute, for which lookups on the name as well as the
75 object are permitted.
77 Notes
78 -----
79 In addition to the new `names` property and `byName` method, this class
80 simply redefines the type signature for `__getitem__` and `get` that would
81 otherwise be inherited from `Mapping`. That is only relevant for static
82 type checking; the actual Python runtime doesn't care about types at all.
83 """
85 __slots__ = ()
87 @property
88 @abstractmethod
89 def names(self) -> AbstractSet[str]:
90 """The set of names associated with the keys, in the same order
91 (`AbstractSet` [ `str` ]).
92 """
93 raise NotImplementedError()
95 def byName(self) -> Dict[str, V_co]:
96 """Return a `Mapping` with names as keys and the same values as
97 ``self``.
99 Returns
100 -------
101 dictionary : `dict`
102 A dictionary with the same values (and iteration order) as
103 ``self``, with `str` names as keys. This is always a new object,
104 not a view.
105 """
106 return dict(zip(self.names, self.values()))
108 @abstractmethod
109 def keys(self) -> NamedValueAbstractSet[K_co]:
110 # TODO: docs
111 raise NotImplementedError()
113 @abstractmethod
114 def __getitem__(self, key: Union[str, K_co]) -> V_co:
115 raise NotImplementedError()
117 def get(self, key: Union[str, K_co], default: Any = None) -> Any:
118 # Delegating to super is not allowed by typing, because it doesn't
119 # accept str, but we know it just delegates to __getitem__, which does.
120 return super().get(key, default) # type: ignore
123NameLookupMapping = Union[NamedKeyMapping[K_co, V_co], Mapping[str, V_co]]
124"""A type annotation alias for signatures that want to use ``mapping[s]``
125(or ``mapping.get(s)``) where ``s`` is a `str`, and don't care whether
126``mapping.keys()`` returns named objects or direct `str` instances.
127"""
130class NamedKeyMutableMapping(NamedKeyMapping[K, V], MutableMapping[K, V]):
131 """An abstract base class that adds mutation to `NamedKeyMapping`.
132 """
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 """A dictionary wrapper that require keys to have a ``.name`` attribute,
151 and permits lookups using either key objects or their names.
153 Names can be used in place of keys when updating existing items, but not
154 when adding new items.
156 It is assumed (but asserted) that all name equality is equivalent to key
157 equality, either because the key objects define equality this way, or
158 because different objects with the same name are never included in the same
159 dictionary.
161 Parameters
162 ----------
163 args
164 All positional constructor arguments are forwarded directly to `dict`.
165 Keyword arguments are not accepted, because plain strings are not valid
166 keys for `NamedKeyDict`.
168 Raises
169 ------
170 AttributeError
171 Raised when an attempt is made to add an object with no ``.name``
172 attribute to the dictionary.
173 AssertionError
174 Raised when multiple keys have the same name.
175 """
177 __slots__ = ("_dict", "_names",)
179 def __init__(self, *args: Any):
180 self._dict: Dict[K, V] = dict(*args)
181 self._names = {key.name: key for key in self._dict}
182 assert len(self._names) == len(self._dict), "Duplicate names in keys."
184 @property
185 def names(self) -> KeysView[str]:
186 """The set of names associated with the keys, in the same order
187 (`~collections.abc.KeysView`).
188 """
189 return self._names.keys()
191 def byName(self) -> Dict[str, V]:
192 """Return a `dict` with names as keys and the same values as ``self``.
193 """
194 return dict(zip(self._names.keys(), self._dict.values()))
196 def __len__(self) -> int:
197 return len(self._dict)
199 def __iter__(self) -> Iterator[K]:
200 return iter(self._dict)
202 def __str__(self) -> str:
203 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items()))
205 def __repr__(self) -> str:
206 return "NamedKeyDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items()))
208 def __getitem__(self, key: Union[str, K]) -> V:
209 if isinstance(key, str):
210 return self._dict[self._names[key]]
211 else:
212 return self._dict[key]
214 def __setitem__(self, key: Union[str, K], value: V) -> None:
215 if isinstance(key, str):
216 self._dict[self._names[key]] = value
217 else:
218 assert self._names.get(key.name, key) == key, "Name is already associated with a different key."
219 self._dict[key] = value
220 self._names[key.name] = key
222 def __delitem__(self, key: Union[str, K]) -> None:
223 if isinstance(key, str):
224 del self._dict[self._names[key]]
225 del self._names[key]
226 else:
227 del self._dict[key]
228 del self._names[key.name]
230 def keys(self) -> NamedValueAbstractSet[K]:
231 return NameMappingSetView(self._names)
233 def values(self) -> ValuesView[V]:
234 return self._dict.values()
236 def items(self) -> ItemsView[K, V]:
237 return self._dict.items()
239 def copy(self) -> NamedKeyDict[K, V]:
240 """Return a new `NamedKeyDict` with the same elements.
241 """
242 result = NamedKeyDict.__new__(NamedKeyDict)
243 result._dict = dict(self._dict)
244 result._names = dict(self._names)
245 return result
247 def freeze(self) -> NamedKeyMapping[K, V]:
248 """Disable all mutators, effectively transforming ``self`` into
249 an immutable mapping.
251 Returns
252 -------
253 self : `NamedKeyMapping`
254 While ``self`` is modified in-place, it is also returned with a
255 type anotation that reflects its new, frozen state; assigning it
256 to a new variable (and considering any previous references
257 invalidated) should allow for more accurate static type checking.
258 """
259 if not isinstance(self._dict, MappingProxyType):
260 self._dict = MappingProxyType(self._dict) # type: ignore
261 return self
264class NamedValueAbstractSet(AbstractSet[K_co]):
265 """An abstract base class for custom sets whose elements are objects with
266 a `str` ``name`` attribute, allowing some dict-like operations and
267 views to be supported.
268 """
270 __slots__ = ()
272 @property
273 @abstractmethod
274 def names(self) -> AbstractSet[str]:
275 """The set of names associated with the keys, in the same order
276 (`AbstractSet` [ `str` ]).
277 """
278 raise NotImplementedError()
280 @abstractmethod
281 def asMapping(self) -> Mapping[str, K_co]:
282 """Return a mapping view with names as keys.
284 Returns
285 -------
286 dict : `Mapping`
287 A dictionary-like view with ``values() == self``.
288 """
289 raise NotImplementedError()
291 @abstractmethod
292 def __getitem__(self, key: Union[str, K_co]) -> K_co:
293 raise NotImplementedError()
295 def get(self, key: Union[str, K_co], default: Any = None) -> Any:
296 """Return the element with the given name, or ``default`` if
297 no such element is present.
298 """
299 try:
300 return self[key]
301 except KeyError:
302 return default
304 @classmethod
305 def _from_iterable(cls, iterable: Iterable[K_co]) -> NamedValueSet[K_co]:
306 """Hook to ensure that inherited `collections.abc.Set` operators return
307 `NamedValueSet` instances, not something else (see `collections.abc`
308 documentation for more information).
310 Note that this behavior can only be guaranteed when both operands are
311 `NamedValueAbstractSet` instances.
312 """
313 return NamedValueSet(iterable)
316class NameMappingSetView(NamedValueAbstractSet[K_co]):
317 """A lightweight implementation of `NamedValueAbstractSet` backed by a
318 mapping from name to named object.
320 Parameters
321 ----------
322 mapping : `Mapping` [ `str`, `object` ]
323 Mapping this object will provide a view of.
324 """
325 def __init__(self, mapping: Mapping[str, K_co]):
326 self._mapping = mapping
328 __slots__ = ("_mapping",)
330 @property
331 def names(self) -> AbstractSet[str]:
332 # Docstring inherited from NamedValueAbstractSet.
333 return self._mapping.keys()
335 def asMapping(self) -> Mapping[str, K_co]:
336 # Docstring inherited from NamedValueAbstractSet.
337 return self._mapping
339 def __getitem__(self, key: Union[str, K_co]) -> K_co:
340 if isinstance(key, str):
341 return self._mapping[key]
342 else:
343 return self._mapping[key.name]
345 def __contains__(self, key: Any) -> bool:
346 return getattr(key, "name", key) in self._mapping
348 def __len__(self) -> int:
349 return len(self._mapping)
351 def __iter__(self) -> Iterator[K_co]:
352 return iter(self._mapping.values())
354 def __eq__(self, other: Any) -> bool:
355 if isinstance(other, NamedValueAbstractSet):
356 return self.names == other.names
357 else:
358 return set(self._mapping.values()) == other
360 def __le__(self, other: AbstractSet[K]) -> bool:
361 if isinstance(other, NamedValueAbstractSet):
362 return self.names <= other.names
363 else:
364 return set(self._mapping.values()) <= other
366 def __ge__(self, other: AbstractSet[K]) -> bool:
367 if isinstance(other, NamedValueAbstractSet):
368 return self.names >= other.names
369 else:
370 return set(self._mapping.values()) >= other
372 def __str__(self) -> str:
373 return "{{{}}}".format(", ".join(str(element) for element in self))
375 def __repr__(self) -> str:
376 return f"NameMappingSetView({self._mapping})"
379class NamedValueMutableSet(NamedValueAbstractSet[K], MutableSet[K]):
380 """An abstract base class that adds mutation interfaces to
381 `NamedValueAbstractSet`.
383 Methods that can add new elements to the set are unchanged from their
384 `MutableSet` definitions, while those that only remove them can generally
385 accept names or element instances. `pop` can be used in either its
386 `MutableSet` form (no arguments; an arbitrary element is returned) or its
387 `MutableMapping` form (one or two arguments for the name and optional
388 default value, respectively). A `MutableMapping`-like `__delitem__`
389 interface is also included, which takes only names (like
390 `NamedValueAbstractSet.__getitem__`).
391 """
393 __slots__ = ()
395 @abstractmethod
396 def __delitem__(self, name: str) -> None:
397 raise NotImplementedError()
399 @abstractmethod
400 def remove(self, element: Union[str, K]) -> Any:
401 """Remove an element from the set.
403 Parameters
404 ----------
405 element : `object` or `str`
406 Element to remove or the string name thereof. Assumed to be an
407 element if it has a ``.name`` attribute.
409 Raises
410 ------
411 KeyError
412 Raised if an element with the given name does not exist.
413 """
414 raise NotImplementedError()
416 @abstractmethod
417 def discard(self, element: Union[str, K]) -> Any:
418 """Remove an element from the set if it exists.
420 Does nothing if no matching element is present.
422 Parameters
423 ----------
424 element : `object` or `str`
425 Element to remove or the string name thereof. Assumed to be an
426 element if it has a ``.name`` attribute.
427 """
428 raise NotImplementedError()
430 @abstractmethod
431 def pop(self, *args: str) -> K:
432 """Remove and return an element from the set.
434 Parameters
435 ----------
436 name : `str`, optional
437 Name of the element to remove and return. Must be passed
438 positionally. If not provided, an arbitrary element is
439 removed and returned.
441 Raises
442 ------
443 KeyError
444 Raised if ``name`` is provided but ``default`` is not, and no
445 matching element exists.
446 """
447 raise NotImplementedError()
450class NamedValueSet(NameMappingSetView[K], NamedValueMutableSet[K]):
451 """A custom mutable set class that requires elements to have a ``.name``
452 attribute, which can then be used as keys in `dict`-like lookup.
454 Names and elements can both be used with the ``in`` and ``del``
455 operators, `remove`, and `discard`. Names (but not elements)
456 can be used with ``[]``-based element retrieval (not assignment)
457 and the `get` method.
459 Parameters
460 ----------
461 elements : `iterable`
462 Iterable over elements to include in the set.
464 Raises
465 ------
466 AttributeError
467 Raised if one or more elements do not have a ``.name`` attribute.
469 Notes
470 -----
471 Iteration order is guaranteed to be the same as insertion order (with
472 the same general behavior as `dict` ordering).
473 Like `dicts`, sets with the same elements will compare as equal even if
474 their iterator order is not the same.
475 """
477 def __init__(self, elements: Iterable[K] = ()):
478 super().__init__({element.name: element for element in elements})
480 def __repr__(self) -> str:
481 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self))
483 def issubset(self, other: AbstractSet[K]) -> bool:
484 return self <= other
486 def issuperset(self, other: AbstractSet[K]) -> bool:
487 return self >= other
489 def __delitem__(self, name: str) -> None:
490 del self._mapping[name]
492 def add(self, element: K) -> None:
493 """Add an element to the set.
495 Raises
496 ------
497 AttributeError
498 Raised if the element does not have a ``.name`` attribute.
499 """
500 self._mapping[element.name] = element
502 def clear(self) -> None:
503 # Docstring inherited.
504 self._mapping.clear()
506 def remove(self, element: Union[str, K]) -> Any:
507 # Docstring inherited.
508 del self._mapping[getattr(element, "name", element)]
510 def discard(self, element: Union[str, K]) -> Any:
511 # Docstring inherited.
512 try:
513 self.remove(element)
514 except KeyError:
515 pass
517 def pop(self, *args: str) -> K:
518 # Docstring inherited.
519 if not args:
520 return super().pop()
521 else:
522 return self._mapping.pop(*args)
524 def update(self, elements: Iterable[K]) -> None:
525 """Add multiple new elements to the set.
527 Parameters
528 ----------
529 elements : `Iterable`
530 Elements to add.
531 """
532 for element in elements:
533 self.add(element)
535 def copy(self) -> NamedValueSet[K]:
536 """Return a new `NamedValueSet` with the same elements.
537 """
538 result = NamedValueSet.__new__(NamedValueSet)
539 result._mapping = dict(self._mapping)
540 return result
542 def freeze(self) -> NamedValueAbstractSet[K]:
543 """Disable all mutators, effectively transforming ``self`` into
544 an immutable set.
546 Returns
547 -------
548 self : `NamedValueAbstractSet`
549 While ``self`` is modified in-place, it is also returned with a
550 type anotation that reflects its new, frozen state; assigning it
551 to a new variable (and considering any previous references
552 invalidated) should allow for more accurate static type checking.
553 """
554 if not isinstance(self._mapping, MappingProxyType):
555 self._mapping = MappingProxyType(self._mapping) # type: ignore
556 return self
558 _mapping: Dict[str, K]