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

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)
64V = TypeVar("V")
67class NamedKeyMapping(Mapping[K, V]):
68 """An abstract base class for custom mappings whose keys are objects with
69 a `str` ``name`` attribute, for which lookups on the name as well as the
70 object are permitted.
72 Notes
73 -----
74 In addition to the new `names` property and `byName` method, this class
75 simply redefines the type signature for `__getitem__` and `get` that would
76 otherwise be inherited from `Mapping`. That is only relevant for static
77 type checking; the actual Python runtime doesn't care about types at all.
78 """
80 @property
81 @abstractmethod
82 def names(self) -> AbstractSet[str]:
83 """The set of names associated with the keys, in the same order
84 (`AbstractSet` [ `str` ]).
85 """
86 raise NotImplementedError()
88 def byName(self) -> Dict[str, V]:
89 """Return a `Mapping` with names as keys and the same values as
90 ``self``.
92 Returns
93 -------
94 dictionary : `dict`
95 A dictionary with the same values (and iteration order) as
96 ``self``, with `str` names as keys. This is always a new object,
97 not a view.
98 """
99 return dict(zip(self.names, self.values()))
101 @abstractmethod
102 def __getitem__(self, key: Union[str, K]) -> V:
103 raise NotImplementedError()
105 def get(self, key: Union[str, K], default: Any = None) -> Any:
106 # Delegating to super is not allowed by typing, because it doesn't
107 # accept str, but we know it just delegates to __getitem__, which does.
108 return super().get(key, default) # type: ignore
111NameLookupMapping = Union[NamedKeyMapping[K, V], Mapping[str, V]]
112"""A type annotation alias for signatures that want to use ``mapping[s]``
113(or ``mapping.get(s)``) where ``s`` is a `str`, and don't care whether
114``mapping.keys()`` returns a named objects or direct `str` instances.
115"""
118class NamedKeyMutableMapping(NamedKeyMapping[K, V], MutableMapping[K, V]):
119 """An abstract base class that adds mutation to `NamedKeyMapping`.
120 """
122 @abstractmethod
123 def __setitem__(self, key: Union[str, K], value: V) -> None:
124 raise NotImplementedError()
126 @abstractmethod
127 def __delitem__(self, key: Union[str, K]) -> None:
128 raise NotImplementedError()
130 def pop(self, key: Union[str, K], default: Any = None) -> Any:
131 # See comment in `NamedKeyMapping.get`; same logic applies here.
132 return super().pop(key, default) # type: ignore
135class NamedKeyDict(NamedKeyMutableMapping[K, V]):
136 """A dictionary wrapper that require keys to have a ``.name`` attribute,
137 and permits lookups using either key objects or their names.
139 Names can be used in place of keys when updating existing items, but not
140 when adding new items.
142 It is assumed (but asserted) that all name equality is equivalent to key
143 equality, either because the key objects define equality this way, or
144 because different objects with the same name are never included in the same
145 dictionary.
147 Parameters
148 ----------
149 args
150 All positional constructor arguments are forwarded directly to `dict`.
151 Keyword arguments are not accepted, because plain strings are not valid
152 keys for `NamedKeyDict`.
154 Raises
155 ------
156 AttributeError
157 Raised when an attempt is made to add an object with no ``.name``
158 attribute to the dictionary.
159 AssertionError
160 Raised when multiple keys have the same name.
161 """
163 __slots__ = ("_dict", "_names",)
165 def __init__(self, *args: Any):
166 self._dict: Dict[K, V] = dict(*args)
167 self._names = {key.name: key for key in self._dict}
168 assert len(self._names) == len(self._dict), "Duplicate names in keys."
170 @property
171 def names(self) -> KeysView[str]:
172 """The set of names associated with the keys, in the same order
173 (`~collections.abc.KeysView`).
174 """
175 return self._names.keys()
177 def byName(self) -> Dict[str, V]:
178 """Return a `dict` with names as keys and the same values as ``self``.
179 """
180 return dict(zip(self._names.keys(), self._dict.values()))
182 def __len__(self) -> int:
183 return len(self._dict)
185 def __iter__(self) -> Iterator[K]:
186 return iter(self._dict)
188 def __str__(self) -> str:
189 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items()))
191 def __repr__(self) -> str:
192 return "NamedKeyDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items()))
194 def __getitem__(self, key: Union[str, K]) -> V:
195 if isinstance(key, str):
196 return self._dict[self._names[key]]
197 else:
198 return self._dict[key]
200 def __setitem__(self, key: Union[str, K], value: V) -> None:
201 if isinstance(key, str):
202 self._dict[self._names[key]] = value
203 else:
204 assert self._names.get(key.name, key) == key, "Name is already associated with a different key."
205 self._dict[key] = value
206 self._names[key.name] = key
208 def __delitem__(self, key: Union[str, K]) -> None:
209 if isinstance(key, str):
210 del self._dict[self._names[key]]
211 del self._names[key]
212 else:
213 del self._dict[key]
214 del self._names[key.name]
216 def keys(self) -> KeysView[K]:
217 return self._dict.keys()
219 def values(self) -> ValuesView[V]:
220 return self._dict.values()
222 def items(self) -> ItemsView[K, V]:
223 return self._dict.items()
225 def copy(self) -> NamedKeyDict[K, V]:
226 result = NamedKeyDict.__new__(NamedKeyDict)
227 result._dict = dict(self._dict)
228 result._names = dict(self._names)
229 return result
231 def freeze(self) -> None:
232 """Disable all mutators, effectively transforming ``self`` into
233 an immutable mapping.
234 """
235 if not isinstance(self._dict, MappingProxyType):
236 self._dict = MappingProxyType(self._dict) # type: ignore
239class NamedValueSet(MutableSet[K]):
240 """A custom mutable set class that requires elements to have a ``.name``
241 attribute, which can then be used as keys in `dict`-like lookup.
243 Names and elements can both be used with the ``in`` and ``del``
244 operators, `remove`, and `discard`. Names (but not elements)
245 can be used with ``[]``-based element retrieval (not assignment)
246 and the `get` method. `pop` can be used in either its `MutableSet`
247 form (no arguments; an arbitrary element is returned) or its
248 `MutableMapping` form (one or two arguments for the name and
249 optional default value, respectively).
251 Parameters
252 ----------
253 elements : `iterable`
254 Iterable over elements to include in the set.
256 Raises
257 ------
258 AttributeError
259 Raised if one or more elements do not have a ``.name`` attribute.
261 Notes
262 -----
263 Iteration order is guaranteed to be the same as insertion order (with
264 the same general behavior as `dict` ordering).
265 Like `dicts`, sets with the same elements will compare as equal even if
266 their iterator order is not the same.
267 """
269 __slots__ = ("_dict",)
271 def __init__(self, elements: Iterable[K] = ()):
272 self._dict = {element.name: element for element in elements}
274 @property
275 def names(self) -> KeysView[str]:
276 """The set of element names, in the same order
277 (`~collections.abc.KeysView`).
278 """
279 return self._dict.keys()
281 def asDict(self) -> Mapping[str, K]:
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 return self._dict
291 def __contains__(self, key: Any) -> bool:
292 return getattr(key, "name", key) in self._dict
294 def __len__(self) -> int:
295 return len(self._dict)
297 def __iter__(self) -> Iterator[K]:
298 return iter(self._dict.values())
300 def __str__(self) -> str:
301 return "{{{}}}".format(", ".join(str(element) for element in self))
303 def __repr__(self) -> str:
304 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self))
306 def __eq__(self, other: Any) -> Union[bool, NotImplemented]:
307 if isinstance(other, NamedValueSet):
308 return self._dict.keys() == other._dict.keys()
309 else:
310 return NotImplemented
312 def __hash__(self) -> int:
313 return hash(frozenset(self._dict.keys()))
315 # As per Set's docs, overriding just __le__ and __ge__ for performance will
316 # cover the other comparisons, too.
318 def __le__(self, other: AbstractSet[K]) -> Union[bool, NotImplemented]:
319 if isinstance(other, NamedValueSet):
320 return self._dict.keys() <= other._dict.keys()
321 else:
322 return NotImplemented
324 def __ge__(self, other: AbstractSet[K]) -> Union[bool, NotImplemented]:
325 if isinstance(other, NamedValueSet):
326 return self._dict.keys() >= other._dict.keys()
327 else:
328 return NotImplemented
330 def issubset(self, other: AbstractSet[K]) -> bool:
331 return self <= other
333 def issuperset(self, other: AbstractSet[K]) -> bool:
334 return self >= other
336 def __getitem__(self, name: str) -> K:
337 return self._dict[name]
339 def get(self, name: str, default: Any = None) -> Any:
340 """Return the element with the given name, or ``default`` if
341 no such element is present.
342 """
343 return self._dict.get(name, default)
345 def __delitem__(self, name: str) -> None:
346 del self._dict[name]
348 def add(self, element: K) -> None:
349 """Add an element to the set.
351 Raises
352 ------
353 AttributeError
354 Raised if the element does not have a ``.name`` attribute.
355 """
356 self._dict[element.name] = element
358 def remove(self, element: Union[str, K]) -> Any:
359 """Remove an element from the set.
361 Parameters
362 ----------
363 element : `object` or `str`
364 Element to remove or the string name thereof. Assumed to be an
365 element if it has a ``.name`` attribute.
367 Raises
368 ------
369 KeyError
370 Raised if an element with the given name does not exist.
371 """
372 del self._dict[getattr(element, "name", element)]
374 def discard(self, element: Union[str, K]) -> Any:
375 """Remove an element from the set if it exists.
377 Does nothing if no matching element is present.
379 Parameters
380 ----------
381 element : `object` or `str`
382 Element to remove or the string name thereof. Assumed to be an
383 element if it has a ``.name`` attribute.
384 """
385 try:
386 self.remove(element)
387 except KeyError:
388 pass
390 def pop(self, *args: str) -> K:
391 """Remove and return an element from the set.
393 Parameters
394 ----------
395 name : `str`, optional
396 Name of the element to remove and return. Must be passed
397 positionally. If not provided, an arbitrary element is
398 removed and returned.
400 Raises
401 ------
402 KeyError
403 Raised if ``name`` is provided but ``default`` is not, and no
404 matching element exists.
405 """
406 if not args:
407 return super().pop()
408 else:
409 return self._dict.pop(*args)
411 def copy(self) -> NamedValueSet[K]:
412 result = NamedValueSet.__new__(NamedValueSet)
413 result._dict = dict(self._dict)
414 return result
416 def freeze(self) -> None:
417 """Disable all mutators, effectively transforming ``self`` into
418 an immutable set.
419 """
420 if not isinstance(self._dict, MappingProxyType):
421 self._dict = MappingProxyType(self._dict) # type: ignore