Coverage for python/lsst/daf/butler/core/named.py: 49%
Shortcuts 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
Shortcuts 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 Protocol,
46 TypeVar,
47 Union,
48 ValuesView,
49)
50from types import MappingProxyType
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_co, 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_co]:
111 # TODO: docs
112 raise NotImplementedError()
114 @abstractmethod
115 def __getitem__(self, key: Union[str, K_co]) -> V_co:
116 raise NotImplementedError()
118 def get(self, key: Union[str, K_co], 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_co, 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__ = ("_dict", "_names",)
181 def __init__(self, *args: Any):
182 self._dict: Dict[K, V] = dict(*args)
183 self._names = {key.name: key for key in self._dict}
184 assert len(self._names) == len(self._dict), "Duplicate names in keys."
186 @property
187 def names(self) -> KeysView[str]:
188 """Return set of names associated with the keys, in the same order.
190 (`~collections.abc.KeysView`).
191 """
192 return self._names.keys()
194 def byName(self) -> Dict[str, V]:
195 """Return a `dict` with names as keys and the ``self`` values."""
196 return dict(zip(self._names.keys(), self._dict.values()))
198 def __len__(self) -> int:
199 return len(self._dict)
201 def __iter__(self) -> Iterator[K]:
202 return iter(self._dict)
204 def __str__(self) -> str:
205 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items()))
207 def __repr__(self) -> str:
208 return "NamedKeyDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items()))
210 def __getitem__(self, key: Union[str, K]) -> V:
211 if isinstance(key, str):
212 return self._dict[self._names[key]]
213 else:
214 return self._dict[key]
216 def __setitem__(self, key: Union[str, K], value: V) -> None:
217 if isinstance(key, str):
218 self._dict[self._names[key]] = value
219 else:
220 assert self._names.get(key.name, key) == key, "Name is already associated with a different key."
221 self._dict[key] = value
222 self._names[key.name] = key
224 def __delitem__(self, key: Union[str, K]) -> None:
225 if isinstance(key, str):
226 del self._dict[self._names[key]]
227 del self._names[key]
228 else:
229 del self._dict[key]
230 del self._names[key.name]
232 def keys(self) -> NamedValueAbstractSet[K]:
233 return NameMappingSetView(self._names)
235 def values(self) -> ValuesView[V]:
236 return self._dict.values()
238 def items(self) -> ItemsView[K, V]:
239 return self._dict.items()
241 def copy(self) -> NamedKeyDict[K, V]:
242 """Return a new `NamedKeyDict` with the same elements."""
243 result = NamedKeyDict.__new__(NamedKeyDict)
244 result._dict = dict(self._dict)
245 result._names = dict(self._names)
246 return result
248 def freeze(self) -> NamedKeyMapping[K, V]:
249 """Disable all mutators.
251 Effectively transforms ``self`` into an immutable mapping.
253 Returns
254 -------
255 self : `NamedKeyMapping`
256 While ``self`` is modified in-place, it is also returned with a
257 type annotation that reflects its new, frozen state; assigning it
258 to a new variable (and considering any previous references
259 invalidated) should allow for more accurate static type checking.
260 """
261 if not isinstance(self._dict, MappingProxyType):
262 self._dict = MappingProxyType(self._dict) # type: ignore
263 return self
266class NamedValueAbstractSet(AbstractSet[K_co]):
267 """Custom sets with named elements.
269 An abstract base class for custom sets whose elements are objects with
270 a `str` ``name`` attribute, allowing some dict-like operations and
271 views to be supported.
272 """
274 __slots__ = ()
276 @property
277 @abstractmethod
278 def names(self) -> AbstractSet[str]:
279 """Return set of names associated with the keys, in the same order.
281 (`AbstractSet` [ `str` ]).
282 """
283 raise NotImplementedError()
285 @abstractmethod
286 def asMapping(self) -> Mapping[str, K_co]:
287 """Return a mapping view with names as keys.
289 Returns
290 -------
291 dict : `Mapping`
292 A dictionary-like view with ``values() == self``.
293 """
294 raise NotImplementedError()
296 @abstractmethod
297 def __getitem__(self, key: Union[str, K_co]) -> K_co:
298 raise NotImplementedError()
300 def get(self, key: Union[str, K_co], default: Any = None) -> Any:
301 """Return the element with the given name.
303 Returns ``default`` if no such element is present.
304 """
305 try:
306 return self[key]
307 except KeyError:
308 return default
310 @classmethod
311 def _from_iterable(cls, iterable: Iterable[K_co]) -> NamedValueSet[K_co]:
312 """Construct class from an iterable.
314 Hook to ensure that inherited `collections.abc.Set` operators return
315 `NamedValueSet` instances, not something else (see `collections.abc`
316 documentation for more information).
318 Note that this behavior can only be guaranteed when both operands are
319 `NamedValueAbstractSet` instances.
320 """
321 return NamedValueSet(iterable)
324class NameMappingSetView(NamedValueAbstractSet[K_co]):
325 """A lightweight implementation of `NamedValueAbstractSet`.
327 Backed by a mapping from name to named object.
329 Parameters
330 ----------
331 mapping : `Mapping` [ `str`, `object` ]
332 Mapping this object will provide a view of.
333 """
335 def __init__(self, mapping: Mapping[str, K_co]):
336 self._mapping = mapping
338 __slots__ = ("_mapping",)
340 @property
341 def names(self) -> AbstractSet[str]:
342 # Docstring inherited from NamedValueAbstractSet.
343 return self._mapping.keys()
345 def asMapping(self) -> Mapping[str, K_co]:
346 # Docstring inherited from NamedValueAbstractSet.
347 return self._mapping
349 def __getitem__(self, key: Union[str, K_co]) -> K_co:
350 if isinstance(key, str):
351 return self._mapping[key]
352 else:
353 return self._mapping[key.name]
355 def __contains__(self, key: Any) -> bool:
356 return getattr(key, "name", key) in self._mapping
358 def __len__(self) -> int:
359 return len(self._mapping)
361 def __iter__(self) -> Iterator[K_co]:
362 return iter(self._mapping.values())
364 def __eq__(self, other: Any) -> bool:
365 if isinstance(other, NamedValueAbstractSet):
366 return self.names == other.names
367 else:
368 return set(self._mapping.values()) == other
370 def __le__(self, other: AbstractSet[K]) -> bool:
371 if isinstance(other, NamedValueAbstractSet):
372 return self.names <= other.names
373 else:
374 return set(self._mapping.values()) <= other
376 def __ge__(self, other: AbstractSet[K]) -> bool:
377 if isinstance(other, NamedValueAbstractSet):
378 return self.names >= other.names
379 else:
380 return set(self._mapping.values()) >= other
382 def __str__(self) -> str:
383 return "{{{}}}".format(", ".join(str(element) for element in self))
385 def __repr__(self) -> str:
386 return f"NameMappingSetView({self._mapping})"
389class NamedValueMutableSet(NamedValueAbstractSet[K], MutableSet[K]):
390 """Mutable variant of `NamedValueAbstractSet`.
392 Methods that can add new elements to the set are unchanged from their
393 `MutableSet` definitions, while those that only remove them can generally
394 accept names or element instances. `pop` can be used in either its
395 `MutableSet` form (no arguments; an arbitrary element is returned) or its
396 `MutableMapping` form (one or two arguments for the name and optional
397 default value, respectively). A `MutableMapping`-like `__delitem__`
398 interface is also included, which takes only names (like
399 `NamedValueAbstractSet.__getitem__`).
400 """
402 __slots__ = ()
404 @abstractmethod
405 def __delitem__(self, name: str) -> None:
406 raise NotImplementedError()
408 @abstractmethod
409 def remove(self, element: Union[str, K]) -> Any:
410 """Remove an element from the set.
412 Parameters
413 ----------
414 element : `object` or `str`
415 Element to remove or the string name thereof. Assumed to be an
416 element if it has a ``.name`` attribute.
418 Raises
419 ------
420 KeyError
421 Raised if an element with the given name does not exist.
422 """
423 raise NotImplementedError()
425 @abstractmethod
426 def discard(self, element: Union[str, K]) -> Any:
427 """Remove an element from the set if it exists.
429 Does nothing if no matching element is present.
431 Parameters
432 ----------
433 element : `object` or `str`
434 Element to remove or the string name thereof. Assumed to be an
435 element if it has a ``.name`` attribute.
436 """
437 raise NotImplementedError()
439 @abstractmethod
440 def pop(self, *args: str) -> K:
441 """Remove and return an element from the set.
443 Parameters
444 ----------
445 name : `str`, optional
446 Name of the element to remove and return. Must be passed
447 positionally. If not provided, an arbitrary element is
448 removed and returned.
450 Raises
451 ------
452 KeyError
453 Raised if ``name`` is provided but ``default`` is not, and no
454 matching element exists.
455 """
456 raise NotImplementedError()
459class NamedValueSet(NameMappingSetView[K], NamedValueMutableSet[K]):
460 """Custom mutable set class.
462 A custom mutable set class that requires elements to have a ``.name``
463 attribute, which can then be used as keys in `dict`-like lookup.
465 Names and elements can both be used with the ``in`` and ``del``
466 operators, `remove`, and `discard`. Names (but not elements)
467 can be used with ``[]``-based element retrieval (not assignment)
468 and the `get` method.
470 Parameters
471 ----------
472 elements : `iterable`
473 Iterable over elements to include in the set.
475 Raises
476 ------
477 AttributeError
478 Raised if one or more elements do not have a ``.name`` attribute.
480 Notes
481 -----
482 Iteration order is guaranteed to be the same as insertion order (with
483 the same general behavior as `dict` ordering).
484 Like `dicts`, sets with the same elements will compare as equal even if
485 their iterator order is not the same.
486 """
488 def __init__(self, elements: Iterable[K] = ()):
489 super().__init__({element.name: element for element in elements})
491 def __repr__(self) -> str:
492 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self))
494 def issubset(self, other: AbstractSet[K]) -> bool:
495 return self <= other
497 def issuperset(self, other: AbstractSet[K]) -> bool:
498 return self >= other
500 def __delitem__(self, name: str) -> None:
501 del self._mapping[name]
503 def add(self, element: K) -> None:
504 """Add an element to the set.
506 Raises
507 ------
508 AttributeError
509 Raised if the element does not have a ``.name`` attribute.
510 """
511 self._mapping[element.name] = element
513 def clear(self) -> None:
514 # Docstring inherited.
515 self._mapping.clear()
517 def remove(self, element: Union[str, K]) -> Any:
518 # Docstring inherited.
519 del self._mapping[getattr(element, "name", element)]
521 def discard(self, element: Union[str, K]) -> Any:
522 # Docstring inherited.
523 try:
524 self.remove(element)
525 except KeyError:
526 pass
528 def pop(self, *args: str) -> K:
529 # Docstring inherited.
530 if not args:
531 return super().pop()
532 else:
533 return self._mapping.pop(*args)
535 def update(self, elements: Iterable[K]) -> None:
536 """Add multiple new elements to the set.
538 Parameters
539 ----------
540 elements : `Iterable`
541 Elements to add.
542 """
543 for element in elements:
544 self.add(element)
546 def copy(self) -> NamedValueSet[K]:
547 """Return a new `NamedValueSet` with the same elements."""
548 result = NamedValueSet.__new__(NamedValueSet)
549 result._mapping = dict(self._mapping)
550 return result
552 def freeze(self) -> NamedValueAbstractSet[K]:
553 """Disable all mutators.
555 Effectively transforming ``self`` into an immutable set.
557 Returns
558 -------
559 self : `NamedValueAbstractSet`
560 While ``self`` is modified in-place, it is also returned with a
561 type annotation that reflects its new, frozen state; assigning it
562 to a new variable (and considering any previous references
563 invalidated) should allow for more accurate static type checking.
564 """
565 if not isinstance(self._mapping, MappingProxyType):
566 self._mapping = MappingProxyType(self._mapping) # type: ignore
567 return self
569 _mapping: Dict[str, K]