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

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
305class NameMappingSetView(NamedValueAbstractSet[K_co]):
306 """A lightweight implementation of `NamedValueAbstractSet` backed by a
307 mapping from name to named object.
309 Parameters
310 ----------
311 mapping : `Mapping` [ `str`, `object` ]
312 Mapping this object will provide a view of.
313 """
314 def __init__(self, mapping: Mapping[str, K_co]):
315 self._mapping = mapping
317 __slots__ = ("_mapping",)
319 @property
320 def names(self) -> AbstractSet[str]:
321 # Docstring inherited from NamedValueAbstractSet.
322 return self._mapping.keys()
324 def asMapping(self) -> Mapping[str, K_co]:
325 # Docstring inherited from NamedValueAbstractSet.
326 return self._mapping
328 def __getitem__(self, key: Union[str, K_co]) -> K_co:
329 if isinstance(key, str):
330 return self._mapping[key]
331 else:
332 return self._mapping[key.name]
334 def __contains__(self, key: Any) -> bool:
335 return getattr(key, "name", key) in self._mapping
337 def __len__(self) -> int:
338 return len(self._mapping)
340 def __iter__(self) -> Iterator[K_co]:
341 return iter(self._mapping.values())
343 def __eq__(self, other: Any) -> bool:
344 if isinstance(other, NamedValueAbstractSet):
345 return self.names == other.names
346 else:
347 return set(self._mapping.values()) == other
349 def __le__(self, other: AbstractSet[K]) -> bool:
350 if isinstance(other, NamedValueAbstractSet):
351 return self.names <= other.names
352 else:
353 return set(self._mapping.values()) <= other
355 def __ge__(self, other: AbstractSet[K]) -> bool:
356 if isinstance(other, NamedValueAbstractSet):
357 return self.names >= other.names
358 else:
359 return set(self._mapping.values()) >= other
361 def __str__(self) -> str:
362 return "{{{}}}".format(", ".join(str(element) for element in self))
364 def __repr__(self) -> str:
365 return f"NameMappingSetView({self._mapping})"
368class NamedValueMutableSet(NamedValueAbstractSet[K], MutableSet[K]):
369 """An abstract base class that adds mutation interfaces to
370 `NamedValueAbstractSet`.
372 Methods that can add new elements to the set are unchanged from their
373 `MutableSet` definitions, while those that only remove them can generally
374 accept names or element instances. `pop` can be used in either its
375 `MutableSet` form (no arguments; an arbitrary element is returned) or its
376 `MutableMapping` form (one or two arguments for the name and optional
377 default value, respectively). A `MutableMapping`-like `__delitem__`
378 interface is also included, which takes only names (like
379 `NamedValueAbstractSet.__getitem__`).
380 """
382 __slots__ = ()
384 @abstractmethod
385 def __delitem__(self, name: str) -> None:
386 raise NotImplementedError()
388 @abstractmethod
389 def remove(self, element: Union[str, K]) -> Any:
390 """Remove an element from the set.
392 Parameters
393 ----------
394 element : `object` or `str`
395 Element to remove or the string name thereof. Assumed to be an
396 element if it has a ``.name`` attribute.
398 Raises
399 ------
400 KeyError
401 Raised if an element with the given name does not exist.
402 """
403 raise NotImplementedError()
405 @abstractmethod
406 def discard(self, element: Union[str, K]) -> Any:
407 """Remove an element from the set if it exists.
409 Does nothing if no matching element is present.
411 Parameters
412 ----------
413 element : `object` or `str`
414 Element to remove or the string name thereof. Assumed to be an
415 element if it has a ``.name`` attribute.
416 """
417 raise NotImplementedError()
419 @abstractmethod
420 def pop(self, *args: str) -> K:
421 """Remove and return an element from the set.
423 Parameters
424 ----------
425 name : `str`, optional
426 Name of the element to remove and return. Must be passed
427 positionally. If not provided, an arbitrary element is
428 removed and returned.
430 Raises
431 ------
432 KeyError
433 Raised if ``name`` is provided but ``default`` is not, and no
434 matching element exists.
435 """
436 raise NotImplementedError()
439class NamedValueSet(NameMappingSetView[K], NamedValueMutableSet[K]):
440 """A custom mutable set class that requires elements to have a ``.name``
441 attribute, which can then be used as keys in `dict`-like lookup.
443 Names and elements can both be used with the ``in`` and ``del``
444 operators, `remove`, and `discard`. Names (but not elements)
445 can be used with ``[]``-based element retrieval (not assignment)
446 and the `get` method.
448 Parameters
449 ----------
450 elements : `iterable`
451 Iterable over elements to include in the set.
453 Raises
454 ------
455 AttributeError
456 Raised if one or more elements do not have a ``.name`` attribute.
458 Notes
459 -----
460 Iteration order is guaranteed to be the same as insertion order (with
461 the same general behavior as `dict` ordering).
462 Like `dicts`, sets with the same elements will compare as equal even if
463 their iterator order is not the same.
464 """
466 def __init__(self, elements: Iterable[K] = ()):
467 super().__init__({element.name: element for element in elements})
469 def __repr__(self) -> str:
470 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self))
472 def issubset(self, other: AbstractSet[K]) -> bool:
473 return self <= other
475 def issuperset(self, other: AbstractSet[K]) -> bool:
476 return self >= other
478 def __delitem__(self, name: str) -> None:
479 del self._mapping[name]
481 def add(self, element: K) -> None:
482 """Add an element to the set.
484 Raises
485 ------
486 AttributeError
487 Raised if the element does not have a ``.name`` attribute.
488 """
489 self._mapping[element.name] = element
491 def remove(self, element: Union[str, K]) -> Any:
492 # Docstring inherited.
493 del self._mapping[getattr(element, "name", element)]
495 def discard(self, element: Union[str, K]) -> Any:
496 # Docstring inherited.
497 try:
498 self.remove(element)
499 except KeyError:
500 pass
502 def pop(self, *args: str) -> K:
503 # Docstring inherited.
504 if not args:
505 return super().pop()
506 else:
507 return self._mapping.pop(*args)
509 def update(self, elements: Iterable[K]) -> None:
510 """Add multple new elements to the set.
512 Parameters
513 ----------
514 elements : `Iterable`
515 Elements to add.
516 """
517 for element in elements:
518 self.add(element)
520 def copy(self) -> NamedValueSet[K]:
521 """Return a new `NamedValueSet` with the same elements.
522 """
523 result = NamedValueSet.__new__(NamedValueSet)
524 result._mapping = dict(self._mapping)
525 return result
527 def freeze(self) -> NamedValueAbstractSet[K]:
528 """Disable all mutators, effectively transforming ``self`` into
529 an immutable set.
531 Returns
532 -------
533 self : `NamedValueAbstractSet`
534 While ``self`` is modified in-place, it is also returned with a
535 type anotation that reflects its new, frozen state; assigning it
536 to a new variable (and considering any previous references
537 invalidated) should allow for more accurate static type checking.
538 """
539 if not isinstance(self._mapping, MappingProxyType):
540 self._mapping = MappingProxyType(self._mapping) # type: ignore
541 return self
543 _mapping: Dict[str, K]