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

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 Protocol,
46 TypeVar,
47 Union,
48 ValuesView,
49)
50from types import MappingProxyType
53class Named(Protocol):
54 """A non-inheritance interface for objects that have a string name that
55 maps directly to their equality comparisons.
56 """
57 @property
58 def name(self) -> str:
59 pass
62K = TypeVar("K", bound=Named)
63K_co = TypeVar("K_co", bound=Named, covariant=True)
64V = TypeVar("V")
65V_co = TypeVar("V_co", covariant=True)
68class NamedKeyMapping(Mapping[K_co, V_co]):
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 __slots__ = ()
83 @property
84 @abstractmethod
85 def names(self) -> AbstractSet[str]:
86 """The set of names associated with the keys, in the same order
87 (`AbstractSet` [ `str` ]).
88 """
89 raise NotImplementedError()
91 def byName(self) -> Dict[str, V_co]:
92 """Return a `Mapping` with names as keys and the same values as
93 ``self``.
95 Returns
96 -------
97 dictionary : `dict`
98 A dictionary with the same values (and iteration order) as
99 ``self``, with `str` names as keys. This is always a new object,
100 not a view.
101 """
102 return dict(zip(self.names, self.values()))
104 @abstractmethod
105 def keys(self) -> NamedValueAbstractSet[K_co]:
106 # TODO: docs
107 raise NotImplementedError()
109 @abstractmethod
110 def __getitem__(self, key: Union[str, K_co]) -> V_co:
111 raise NotImplementedError()
113 def get(self, key: Union[str, K_co], default: Any = None) -> Any:
114 # Delegating to super is not allowed by typing, because it doesn't
115 # accept str, but we know it just delegates to __getitem__, which does.
116 return super().get(key, default) # type: ignore
119NameLookupMapping = Union[NamedKeyMapping[K_co, V_co], Mapping[str, V_co]]
120"""A type annotation alias for signatures that want to use ``mapping[s]``
121(or ``mapping.get(s)``) where ``s`` is a `str`, and don't care whether
122``mapping.keys()`` returns named objects or direct `str` instances.
123"""
126class NamedKeyMutableMapping(NamedKeyMapping[K, V], MutableMapping[K, V]):
127 """An abstract base class that adds mutation to `NamedKeyMapping`.
128 """
130 __slots__ = ()
132 @abstractmethod
133 def __setitem__(self, key: Union[str, K], value: V) -> None:
134 raise NotImplementedError()
136 @abstractmethod
137 def __delitem__(self, key: Union[str, K]) -> None:
138 raise NotImplementedError()
140 def pop(self, key: Union[str, K], default: Any = None) -> Any:
141 # See comment in `NamedKeyMapping.get`; same logic applies here.
142 return super().pop(key, default) # type: ignore
145class NamedKeyDict(NamedKeyMutableMapping[K, V]):
146 """A dictionary wrapper that require keys to have a ``.name`` attribute,
147 and permits lookups using either key objects or their names.
149 Names can be used in place of keys when updating existing items, but not
150 when adding new items.
152 It is assumed (but asserted) that all name equality is equivalent to key
153 equality, either because the key objects define equality this way, or
154 because different objects with the same name are never included in the same
155 dictionary.
157 Parameters
158 ----------
159 args
160 All positional constructor arguments are forwarded directly to `dict`.
161 Keyword arguments are not accepted, because plain strings are not valid
162 keys for `NamedKeyDict`.
164 Raises
165 ------
166 AttributeError
167 Raised when an attempt is made to add an object with no ``.name``
168 attribute to the dictionary.
169 AssertionError
170 Raised when multiple keys have the same name.
171 """
173 __slots__ = ("_dict", "_names",)
175 def __init__(self, *args: Any):
176 self._dict: Dict[K, V] = dict(*args)
177 self._names = {key.name: key for key in self._dict}
178 assert len(self._names) == len(self._dict), "Duplicate names in keys."
180 @property
181 def names(self) -> KeysView[str]:
182 """The set of names associated with the keys, in the same order
183 (`~collections.abc.KeysView`).
184 """
185 return self._names.keys()
187 def byName(self) -> Dict[str, V]:
188 """Return a `dict` with names as keys and the same values as ``self``.
189 """
190 return dict(zip(self._names.keys(), self._dict.values()))
192 def __len__(self) -> int:
193 return len(self._dict)
195 def __iter__(self) -> Iterator[K]:
196 return iter(self._dict)
198 def __str__(self) -> str:
199 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items()))
201 def __repr__(self) -> str:
202 return "NamedKeyDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items()))
204 def __getitem__(self, key: Union[str, K]) -> V:
205 if isinstance(key, str):
206 return self._dict[self._names[key]]
207 else:
208 return self._dict[key]
210 def __setitem__(self, key: Union[str, K], value: V) -> None:
211 if isinstance(key, str):
212 self._dict[self._names[key]] = value
213 else:
214 assert self._names.get(key.name, key) == key, "Name is already associated with a different key."
215 self._dict[key] = value
216 self._names[key.name] = key
218 def __delitem__(self, key: Union[str, K]) -> None:
219 if isinstance(key, str):
220 del self._dict[self._names[key]]
221 del self._names[key]
222 else:
223 del self._dict[key]
224 del self._names[key.name]
226 def keys(self) -> NamedValueAbstractSet[K]:
227 return NameMappingSetView(self._names)
229 def values(self) -> ValuesView[V]:
230 return self._dict.values()
232 def items(self) -> ItemsView[K, V]:
233 return self._dict.items()
235 def copy(self) -> NamedKeyDict[K, V]:
236 """Return a new `NamedKeyDict` with the same elements.
237 """
238 result = NamedKeyDict.__new__(NamedKeyDict)
239 result._dict = dict(self._dict)
240 result._names = dict(self._names)
241 return result
243 def freeze(self) -> NamedKeyMapping[K, V]:
244 """Disable all mutators, effectively transforming ``self`` into
245 an immutable mapping.
247 Returns
248 -------
249 self : `NamedKeyMapping`
250 While ``self`` is modified in-place, it is also returned with a
251 type anotation that reflects its new, frozen state; assigning it
252 to a new variable (and considering any previous references
253 invalidated) should allow for more accurate static type checking.
254 """
255 if not isinstance(self._dict, MappingProxyType):
256 self._dict = MappingProxyType(self._dict) # type: ignore
257 return self
260class NamedValueAbstractSet(AbstractSet[K_co]):
261 """An abstract base class for custom sets whose elements are objects with
262 a `str` ``name`` attribute, allowing some dict-like operations and
263 views to be supported.
264 """
266 __slots__ = ()
268 @property
269 @abstractmethod
270 def names(self) -> AbstractSet[str]:
271 """The set of names associated with the keys, in the same order
272 (`AbstractSet` [ `str` ]).
273 """
274 raise NotImplementedError()
276 @abstractmethod
277 def asMapping(self) -> Mapping[str, K_co]:
278 """Return a mapping view with names as keys.
280 Returns
281 -------
282 dict : `Mapping`
283 A dictionary-like view with ``values() == self``.
284 """
285 raise NotImplementedError()
287 @abstractmethod
288 def __getitem__(self, key: Union[str, K_co]) -> K_co:
289 raise NotImplementedError()
291 def get(self, key: Union[str, K_co], default: Any = None) -> Any:
292 """Return the element with the given name, or ``default`` if
293 no such element is present.
294 """
295 try:
296 return self[key]
297 except KeyError:
298 return default
300 @classmethod
301 def _from_iterable(cls, iterable: Iterable[K_co]) -> NamedValueSet[K_co]:
302 """Hook to ensure that inherited `collections.abc.Set` operators return
303 `NamedValueSet` instances, not something else (see `collections.abc`
304 documentation for more information).
306 Note that this behavior can only be guaranteed when both operands are
307 `NamedValueAbstractSet` instances.
308 """
309 return NamedValueSet(iterable)
312class NameMappingSetView(NamedValueAbstractSet[K_co]):
313 """A lightweight implementation of `NamedValueAbstractSet` backed by a
314 mapping from name to named object.
316 Parameters
317 ----------
318 mapping : `Mapping` [ `str`, `object` ]
319 Mapping this object will provide a view of.
320 """
321 def __init__(self, mapping: Mapping[str, K_co]):
322 self._mapping = mapping
324 __slots__ = ("_mapping",)
326 @property
327 def names(self) -> AbstractSet[str]:
328 # Docstring inherited from NamedValueAbstractSet.
329 return self._mapping.keys()
331 def asMapping(self) -> Mapping[str, K_co]:
332 # Docstring inherited from NamedValueAbstractSet.
333 return self._mapping
335 def __getitem__(self, key: Union[str, K_co]) -> K_co:
336 if isinstance(key, str):
337 return self._mapping[key]
338 else:
339 return self._mapping[key.name]
341 def __contains__(self, key: Any) -> bool:
342 return getattr(key, "name", key) in self._mapping
344 def __len__(self) -> int:
345 return len(self._mapping)
347 def __iter__(self) -> Iterator[K_co]:
348 return iter(self._mapping.values())
350 def __eq__(self, other: Any) -> bool:
351 if isinstance(other, NamedValueAbstractSet):
352 return self.names == other.names
353 else:
354 return set(self._mapping.values()) == other
356 def __le__(self, other: AbstractSet[K]) -> bool:
357 if isinstance(other, NamedValueAbstractSet):
358 return self.names <= other.names
359 else:
360 return set(self._mapping.values()) <= other
362 def __ge__(self, other: AbstractSet[K]) -> bool:
363 if isinstance(other, NamedValueAbstractSet):
364 return self.names >= other.names
365 else:
366 return set(self._mapping.values()) >= other
368 def __str__(self) -> str:
369 return "{{{}}}".format(", ".join(str(element) for element in self))
371 def __repr__(self) -> str:
372 return f"NameMappingSetView({self._mapping})"
375class NamedValueMutableSet(NamedValueAbstractSet[K], MutableSet[K]):
376 """An abstract base class that adds mutation interfaces to
377 `NamedValueAbstractSet`.
379 Methods that can add new elements to the set are unchanged from their
380 `MutableSet` definitions, while those that only remove them can generally
381 accept names or element instances. `pop` can be used in either its
382 `MutableSet` form (no arguments; an arbitrary element is returned) or its
383 `MutableMapping` form (one or two arguments for the name and optional
384 default value, respectively). A `MutableMapping`-like `__delitem__`
385 interface is also included, which takes only names (like
386 `NamedValueAbstractSet.__getitem__`).
387 """
389 __slots__ = ()
391 @abstractmethod
392 def __delitem__(self, name: str) -> None:
393 raise NotImplementedError()
395 @abstractmethod
396 def remove(self, element: Union[str, K]) -> Any:
397 """Remove an element from the set.
399 Parameters
400 ----------
401 element : `object` or `str`
402 Element to remove or the string name thereof. Assumed to be an
403 element if it has a ``.name`` attribute.
405 Raises
406 ------
407 KeyError
408 Raised if an element with the given name does not exist.
409 """
410 raise NotImplementedError()
412 @abstractmethod
413 def discard(self, element: Union[str, K]) -> Any:
414 """Remove an element from the set if it exists.
416 Does nothing if no matching element is present.
418 Parameters
419 ----------
420 element : `object` or `str`
421 Element to remove or the string name thereof. Assumed to be an
422 element if it has a ``.name`` attribute.
423 """
424 raise NotImplementedError()
426 @abstractmethod
427 def pop(self, *args: str) -> K:
428 """Remove and return an element from the set.
430 Parameters
431 ----------
432 name : `str`, optional
433 Name of the element to remove and return. Must be passed
434 positionally. If not provided, an arbitrary element is
435 removed and returned.
437 Raises
438 ------
439 KeyError
440 Raised if ``name`` is provided but ``default`` is not, and no
441 matching element exists.
442 """
443 raise NotImplementedError()
446class NamedValueSet(NameMappingSetView[K], NamedValueMutableSet[K]):
447 """A custom mutable set class that requires elements to have a ``.name``
448 attribute, which can then be used as keys in `dict`-like lookup.
450 Names and elements can both be used with the ``in`` and ``del``
451 operators, `remove`, and `discard`. Names (but not elements)
452 can be used with ``[]``-based element retrieval (not assignment)
453 and the `get` method.
455 Parameters
456 ----------
457 elements : `iterable`
458 Iterable over elements to include in the set.
460 Raises
461 ------
462 AttributeError
463 Raised if one or more elements do not have a ``.name`` attribute.
465 Notes
466 -----
467 Iteration order is guaranteed to be the same as insertion order (with
468 the same general behavior as `dict` ordering).
469 Like `dicts`, sets with the same elements will compare as equal even if
470 their iterator order is not the same.
471 """
473 def __init__(self, elements: Iterable[K] = ()):
474 super().__init__({element.name: element for element in elements})
476 def __repr__(self) -> str:
477 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self))
479 def issubset(self, other: AbstractSet[K]) -> bool:
480 return self <= other
482 def issuperset(self, other: AbstractSet[K]) -> bool:
483 return self >= other
485 def __delitem__(self, name: str) -> None:
486 del self._mapping[name]
488 def add(self, element: K) -> None:
489 """Add an element to the set.
491 Raises
492 ------
493 AttributeError
494 Raised if the element does not have a ``.name`` attribute.
495 """
496 self._mapping[element.name] = element
498 def clear(self) -> None:
499 # Docstring inherited.
500 self._mapping.clear()
502 def remove(self, element: Union[str, K]) -> Any:
503 # Docstring inherited.
504 del self._mapping[getattr(element, "name", element)]
506 def discard(self, element: Union[str, K]) -> Any:
507 # Docstring inherited.
508 try:
509 self.remove(element)
510 except KeyError:
511 pass
513 def pop(self, *args: str) -> K:
514 # Docstring inherited.
515 if not args:
516 return super().pop()
517 else:
518 return self._mapping.pop(*args)
520 def update(self, elements: Iterable[K]) -> None:
521 """Add multiple new elements to the set.
523 Parameters
524 ----------
525 elements : `Iterable`
526 Elements to add.
527 """
528 for element in elements:
529 self.add(element)
531 def copy(self) -> NamedValueSet[K]:
532 """Return a new `NamedValueSet` with the same elements.
533 """
534 result = NamedValueSet.__new__(NamedValueSet)
535 result._mapping = dict(self._mapping)
536 return result
538 def freeze(self) -> NamedValueAbstractSet[K]:
539 """Disable all mutators, effectively transforming ``self`` into
540 an immutable set.
542 Returns
543 -------
544 self : `NamedValueAbstractSet`
545 While ``self`` is modified in-place, it is also returned with a
546 type anotation that reflects its new, frozen state; assigning it
547 to a new variable (and considering any previous references
548 invalidated) should allow for more accurate static type checking.
549 """
550 if not isinstance(self._mapping, MappingProxyType):
551 self._mapping = MappingProxyType(self._mapping) # type: ignore
552 return self
554 _mapping: Dict[str, K]