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

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 "NameLookupMapping",
27 "NamedValueSet",
28)
30from abc import abstractmethod
31from typing import (
32 AbstractSet,
33 Any,
34 Dict,
35 ItemsView,
36 Iterable,
37 Iterator,
38 KeysView,
39 Mapping,
40 MutableMapping,
41 MutableSet,
42 TypeVar,
43 Union,
44 ValuesView,
45)
46from types import MappingProxyType
47try:
48 # If we're running mypy, we should have typing_extensions.
49 # If we aren't running mypy, we shouldn't assume we do.
50 # When we're safely on Python 3.8, we can import Protocol
51 # from typing and avoid all of this.
52 from typing_extensions import Protocol
54 class Named(Protocol):
55 @property
56 def name(self) -> str:
57 pass
59except ImportError:
60 Named = Any # type: ignore
63K = TypeVar("K", bound=Named)
64K_co = TypeVar("K_co", bound=Named, covariant=True)
65V = TypeVar("V")
66V_co = TypeVar("V_co", covariant=True)
69class NamedKeyMapping(Mapping[K_co, V_co]):
70 """An abstract base class for custom mappings whose keys are objects with
71 a `str` ``name`` attribute, for which lookups on the name as well as the
72 object are permitted.
74 Notes
75 -----
76 In addition to the new `names` property and `byName` method, this class
77 simply redefines the type signature for `__getitem__` and `get` that would
78 otherwise be inherited from `Mapping`. That is only relevant for static
79 type checking; the actual Python runtime doesn't care about types at all.
80 """
82 __slots__ = ()
84 @property
85 @abstractmethod
86 def names(self) -> AbstractSet[str]:
87 """The set of names associated with the keys, in the same order
88 (`AbstractSet` [ `str` ]).
89 """
90 raise NotImplementedError()
92 def byName(self) -> Dict[str, V_co]:
93 """Return a `Mapping` with names as keys and the same values as
94 ``self``.
96 Returns
97 -------
98 dictionary : `dict`
99 A dictionary with the same values (and iteration order) as
100 ``self``, with `str` names as keys. This is always a new object,
101 not a view.
102 """
103 return dict(zip(self.names, self.values()))
105 @abstractmethod
106 def __getitem__(self, key: Union[str, K_co]) -> V_co:
107 raise NotImplementedError()
109 def get(self, key: Union[str, K_co], default: Any = None) -> Any:
110 # Delegating to super is not allowed by typing, because it doesn't
111 # accept str, but we know it just delegates to __getitem__, which does.
112 return super().get(key, default) # type: ignore
115NameLookupMapping = Union[NamedKeyMapping[K_co, V_co], Mapping[str, V_co]]
116"""A type annotation alias for signatures that want to use ``mapping[s]``
117(or ``mapping.get(s)``) where ``s`` is a `str`, and don't care whether
118``mapping.keys()`` returns named objects or direct `str` instances.
119"""
122class NamedKeyMutableMapping(NamedKeyMapping[K, V], MutableMapping[K, V]):
123 """An abstract base class that adds mutation to `NamedKeyMapping`.
124 """
126 __slots__ = ()
128 @abstractmethod
129 def __setitem__(self, key: Union[str, K], value: V) -> None:
130 raise NotImplementedError()
132 @abstractmethod
133 def __delitem__(self, key: Union[str, K]) -> None:
134 raise NotImplementedError()
136 def pop(self, key: Union[str, K], default: Any = None) -> Any:
137 # See comment in `NamedKeyMapping.get`; same logic applies here.
138 return super().pop(key, default) # type: ignore
141class NamedKeyDict(NamedKeyMutableMapping[K, V]):
142 """A dictionary wrapper that require keys to have a ``.name`` attribute,
143 and permits lookups using either key objects or their names.
145 Names can be used in place of keys when updating existing items, but not
146 when adding new items.
148 It is assumed (but asserted) that all name equality is equivalent to key
149 equality, either because the key objects define equality this way, or
150 because different objects with the same name are never included in the same
151 dictionary.
153 Parameters
154 ----------
155 args
156 All positional constructor arguments are forwarded directly to `dict`.
157 Keyword arguments are not accepted, because plain strings are not valid
158 keys for `NamedKeyDict`.
160 Raises
161 ------
162 AttributeError
163 Raised when an attempt is made to add an object with no ``.name``
164 attribute to the dictionary.
165 AssertionError
166 Raised when multiple keys have the same name.
167 """
169 __slots__ = ("_dict", "_names",)
171 def __init__(self, *args: Any):
172 self._dict: Dict[K, V] = dict(*args)
173 self._names = {key.name: key for key in self._dict}
174 assert len(self._names) == len(self._dict), "Duplicate names in keys."
176 @property
177 def names(self) -> KeysView[str]:
178 """The set of names associated with the keys, in the same order
179 (`~collections.abc.KeysView`).
180 """
181 return self._names.keys()
183 def byName(self) -> Dict[str, V]:
184 """Return a `dict` with names as keys and the same values as ``self``.
185 """
186 return dict(zip(self._names.keys(), self._dict.values()))
188 def __len__(self) -> int:
189 return len(self._dict)
191 def __iter__(self) -> Iterator[K]:
192 return iter(self._dict)
194 def __str__(self) -> str:
195 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items()))
197 def __repr__(self) -> str:
198 return "NamedKeyDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items()))
200 def __getitem__(self, key: Union[str, K]) -> V:
201 if isinstance(key, str):
202 return self._dict[self._names[key]]
203 else:
204 return self._dict[key]
206 def __setitem__(self, key: Union[str, K], value: V) -> None:
207 if isinstance(key, str):
208 self._dict[self._names[key]] = value
209 else:
210 assert self._names.get(key.name, key) == key, "Name is already associated with a different key."
211 self._dict[key] = value
212 self._names[key.name] = key
214 def __delitem__(self, key: Union[str, K]) -> None:
215 if isinstance(key, str):
216 del self._dict[self._names[key]]
217 del self._names[key]
218 else:
219 del self._dict[key]
220 del self._names[key.name]
222 def keys(self) -> KeysView[K]:
223 return self._dict.keys()
225 def values(self) -> ValuesView[V]:
226 return self._dict.values()
228 def items(self) -> ItemsView[K, V]:
229 return self._dict.items()
231 def copy(self) -> NamedKeyDict[K, V]:
232 """Return a new `NamedKeyDict` with the same elements.
233 """
234 result = NamedKeyDict.__new__(NamedKeyDict)
235 result._dict = dict(self._dict)
236 result._names = dict(self._names)
237 return result
239 def freeze(self) -> NamedKeyMapping[K, V]:
240 """Disable all mutators, effectively transforming ``self`` into
241 an immutable mapping.
243 Returns
244 -------
245 self : `NamedKeyMapping`
246 While ``self`` is modified in-place, it is also returned with a
247 type anotation that reflects its new, frozen state; assigning it
248 to a new variable (and considering any previous references
249 invalidated) should allow for more accurate static type checking.
250 """
251 if not isinstance(self._dict, MappingProxyType):
252 self._dict = MappingProxyType(self._dict) # type: ignore
253 return self
256class NamedValueAbstractSet(AbstractSet[K_co]):
257 """An abstract base class for custom sets whose elements are objects with
258 a `str` ``name`` attribute, allowing some dict-like operations and
259 views to be supported.
260 """
262 __slots__ = ()
264 @property
265 @abstractmethod
266 def names(self) -> AbstractSet[str]:
267 """The set of names associated with the keys, in the same order
268 (`AbstractSet` [ `str` ]).
269 """
270 raise NotImplementedError()
272 @abstractmethod
273 def asMapping(self) -> Mapping[str, K_co]:
274 """Return a mapping view with names as keys.
276 Returns
277 -------
278 dict : `Mapping`
279 A dictionary-like view with ``values() == self``.
280 """
281 raise NotImplementedError()
283 @abstractmethod
284 def __getitem__(self, key: Union[str, K_co]) -> K_co:
285 raise NotImplementedError()
287 def get(self, key: Union[str, K_co], default: Any = None) -> Any:
288 """Return the element with the given name, or ``default`` if
289 no such element is present.
290 """
291 try:
292 return self[key]
293 except KeyError:
294 return default
297class NamedValueMutableSet(NamedValueAbstractSet[K], MutableSet[K]):
298 """An abstract base class that adds mutation interfaces to
299 `NamedValueAbstractSet`.
301 Methods that can add new elements to the set are unchanged from their
302 `MutableSet` definitions, while those that only remove them can generally
303 accept names or element instances. `pop` can be used in either its
304 `MutableSet` form (no arguments; an arbitrary element is returned) or its
305 `MutableMapping` form (one or two arguments for the name and optional
306 default value, respectively). A `MutableMapping`-like `__delitem__`
307 interface is also included, which takes only names (like
308 `NamedValueAbstractSet.__getitem__`).
309 """
311 __slots__ = ()
313 @abstractmethod
314 def __delitem__(self, name: str) -> None:
315 raise NotImplementedError()
317 @abstractmethod
318 def remove(self, element: Union[str, K]) -> Any:
319 """Remove an element from the set.
321 Parameters
322 ----------
323 element : `object` or `str`
324 Element to remove or the string name thereof. Assumed to be an
325 element if it has a ``.name`` attribute.
327 Raises
328 ------
329 KeyError
330 Raised if an element with the given name does not exist.
331 """
332 raise NotImplementedError()
334 @abstractmethod
335 def discard(self, element: Union[str, K]) -> Any:
336 """Remove an element from the set if it exists.
338 Does nothing if no matching element is present.
340 Parameters
341 ----------
342 element : `object` or `str`
343 Element to remove or the string name thereof. Assumed to be an
344 element if it has a ``.name`` attribute.
345 """
346 raise NotImplementedError()
348 @abstractmethod
349 def pop(self, *args: str) -> K:
350 """Remove and return an element from the set.
352 Parameters
353 ----------
354 name : `str`, optional
355 Name of the element to remove and return. Must be passed
356 positionally. If not provided, an arbitrary element is
357 removed and returned.
359 Raises
360 ------
361 KeyError
362 Raised if ``name`` is provided but ``default`` is not, and no
363 matching element exists.
364 """
365 raise NotImplementedError()
368class NamedValueSet(NamedValueMutableSet[K]):
369 """A custom mutable set class that requires elements to have a ``.name``
370 attribute, which can then be used as keys in `dict`-like lookup.
372 Names and elements can both be used with the ``in`` and ``del``
373 operators, `remove`, and `discard`. Names (but not elements)
374 can be used with ``[]``-based element retrieval (not assignment)
375 and the `get` method.
377 Parameters
378 ----------
379 elements : `iterable`
380 Iterable over elements to include in the set.
382 Raises
383 ------
384 AttributeError
385 Raised if one or more elements do not have a ``.name`` attribute.
387 Notes
388 -----
389 Iteration order is guaranteed to be the same as insertion order (with
390 the same general behavior as `dict` ordering).
391 Like `dicts`, sets with the same elements will compare as equal even if
392 their iterator order is not the same.
393 """
395 __slots__ = ("_dict",)
397 def __init__(self, elements: Iterable[K] = ()):
398 self._dict = {element.name: element for element in elements}
400 @property
401 def names(self) -> KeysView[str]:
402 # Docstring inherited.
403 return self._dict.keys()
405 def asMapping(self) -> Mapping[str, K]:
406 # Docstring inherited.
407 return self._dict
409 def __contains__(self, key: Any) -> bool:
410 return getattr(key, "name", key) in self._dict
412 def __len__(self) -> int:
413 return len(self._dict)
415 def __iter__(self) -> Iterator[K]:
416 return iter(self._dict.values())
418 def __str__(self) -> str:
419 return "{{{}}}".format(", ".join(str(element) for element in self))
421 def __repr__(self) -> str:
422 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self))
424 def __eq__(self, other: Any) -> bool:
425 if isinstance(other, NamedValueSet):
426 return self._dict.keys() == other._dict.keys()
427 else:
428 return NotImplemented
430 def __hash__(self) -> int:
431 return hash(frozenset(self._dict.keys()))
433 # As per Set's docs, overriding just __le__ and __ge__ for performance will
434 # cover the other comparisons, too.
436 def __le__(self, other: AbstractSet[K]) -> bool:
437 if isinstance(other, NamedValueSet):
438 return self._dict.keys() <= other._dict.keys()
439 else:
440 return NotImplemented
442 def __ge__(self, other: AbstractSet[K]) -> bool:
443 if isinstance(other, NamedValueSet):
444 return self._dict.keys() >= other._dict.keys()
445 else:
446 return NotImplemented
448 def issubset(self, other: AbstractSet[K]) -> bool:
449 return self <= other
451 def issuperset(self, other: AbstractSet[K]) -> bool:
452 return self >= other
454 def __getitem__(self, key: Union[str, K]) -> K:
455 if isinstance(key, str):
456 return self._dict[key]
457 else:
458 return self._dict[key.name]
460 def get(self, key: Union[str, K], default: Any = None) -> Any:
461 # Docstring inherited
462 if isinstance(key, str):
463 return self._dict.get(key, default)
464 else:
465 return self._dict.get(key.name, default)
467 def __delitem__(self, name: str) -> None:
468 del self._dict[name]
470 def add(self, element: K) -> None:
471 """Add an element to the set.
473 Raises
474 ------
475 AttributeError
476 Raised if the element does not have a ``.name`` attribute.
477 """
478 self._dict[element.name] = element
480 def remove(self, element: Union[str, K]) -> Any:
481 # Docstring inherited.
482 del self._dict[getattr(element, "name", element)]
484 def discard(self, element: Union[str, K]) -> Any:
485 # Docstring inherited.
486 try:
487 self.remove(element)
488 except KeyError:
489 pass
491 def pop(self, *args: str) -> K:
492 # Docstring inherited.
493 if not args:
494 return super().pop()
495 else:
496 return self._dict.pop(*args)
498 def update(self, elements: Iterable[K]) -> None:
499 """Add multple new elements to the set.
501 Parameters
502 ----------
503 elements : `Iterable`
504 Elements to add.
505 """
506 for element in elements:
507 self.add(element)
509 def copy(self) -> NamedValueSet[K]:
510 """Return a new `NamedValueSet` with the same elements.
511 """
512 result = NamedValueSet.__new__(NamedValueSet)
513 result._dict = dict(self._dict)
514 return result
516 def freeze(self) -> NamedValueAbstractSet[K]:
517 """Disable all mutators, effectively transforming ``self`` into
518 an immutable set.
520 Returns
521 -------
522 self : `NamedValueAbstractSet`
523 While ``self`` is modified in-place, it is also returned with a
524 type anotation that reflects its new, frozen state; assigning it
525 to a new variable (and considering any previous references
526 invalidated) should allow for more accurate static type checking.
527 """
528 if not isinstance(self._dict, MappingProxyType):
529 self._dict = MappingProxyType(self._dict) # type: ignore
530 return self