Coverage for python/lsst/daf/butler/core/storageClass.py: 45%
357 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-25 15:14 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-25 15:14 +0000
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/>.
22"""Support for Storage Classes."""
24from __future__ import annotations
26__all__ = ("StorageClass", "StorageClassFactory", "StorageClassConfig")
28import builtins
29import itertools
30import logging
31from collections import ChainMap
32from collections.abc import (
33 Callable,
34 Collection,
35 ItemsView,
36 Iterator,
37 KeysView,
38 Mapping,
39 Sequence,
40 Set,
41 ValuesView,
42)
43from typing import Any
45from lsst.utils import doImportType
46from lsst.utils.classes import Singleton
47from lsst.utils.introspection import get_full_type_name
49from .config import Config, ConfigSubset
50from .configSupport import LookupKey
51from .storageClassDelegate import StorageClassDelegate
53log = logging.getLogger(__name__)
56class StorageClassConfig(ConfigSubset):
57 """Configuration class for defining Storage Classes."""
59 component = "storageClasses"
60 defaultConfigFile = "storageClasses.yaml"
63class StorageClass:
64 """Class describing how a label maps to a particular Python type.
66 Parameters
67 ----------
68 name : `str`
69 Name to use for this class.
70 pytype : `type` or `str`
71 Python type (or name of type) to associate with the `StorageClass`
72 components : `dict`, optional
73 `dict` mapping name of a component to another `StorageClass`.
74 derivedComponents : `dict`, optional
75 `dict` mapping name of a derived component to another `StorageClass`.
76 parameters : `~collections.abc.Sequence` or `~collections.abc.Set`
77 Parameters understood by this `StorageClass` that can control
78 reading of data from datastores.
79 delegate : `str`, optional
80 Fully qualified name of class supporting assembly and disassembly
81 of a `pytype` instance.
82 converters : `dict` [`str`, `str`], optional
83 Mapping of python type to function that can be called to convert
84 that python type to the valid type of this storage class.
85 """
87 _cls_name: str = "BaseStorageClass"
88 _cls_components: dict[str, StorageClass] | None = None
89 _cls_derivedComponents: dict[str, StorageClass] | None = None
90 _cls_parameters: Set[str] | Sequence[str] | None = None
91 _cls_delegate: str | None = None
92 _cls_pytype: type | str | None = None
93 _cls_converters: dict[str, str] | None = None
94 defaultDelegate: type = StorageClassDelegate
95 defaultDelegateName: str = get_full_type_name(defaultDelegate)
97 def __init__(
98 self,
99 name: str | None = None,
100 pytype: type | str | None = None,
101 components: dict[str, StorageClass] | None = None,
102 derivedComponents: dict[str, StorageClass] | None = None,
103 parameters: Sequence[str] | Set[str] | None = None,
104 delegate: str | None = None,
105 converters: dict[str, str] | None = None,
106 ):
107 if name is None:
108 name = self._cls_name
109 if pytype is None: 109 ↛ 111line 109 didn't jump to line 111, because the condition on line 109 was never false
110 pytype = self._cls_pytype
111 if components is None: 111 ↛ 113line 111 didn't jump to line 113, because the condition on line 111 was never false
112 components = self._cls_components
113 if derivedComponents is None: 113 ↛ 115line 113 didn't jump to line 115, because the condition on line 113 was never false
114 derivedComponents = self._cls_derivedComponents
115 if parameters is None: 115 ↛ 117line 115 didn't jump to line 117, because the condition on line 115 was never false
116 parameters = self._cls_parameters
117 if delegate is None: 117 ↛ 121line 117 didn't jump to line 121, because the condition on line 117 was never false
118 delegate = self._cls_delegate
120 # Merge converters with class defaults.
121 self._converters = {}
122 if self._cls_converters is not None:
123 self._converters.update(self._cls_converters)
124 if converters: 124 ↛ 125line 124 didn't jump to line 125, because the condition on line 124 was never true
125 self._converters.update(converters)
127 # Version of converters where the python types have been
128 # Do not try to import anything until needed.
129 self._converters_by_type: dict[type, Callable[[Any], Any]] | None = None
131 self.name = name
133 if pytype is None:
134 pytype = object
136 self._pytype: type | None
137 if not isinstance(pytype, str):
138 # Already have a type so store it and get the name
139 self._pytypeName = get_full_type_name(pytype)
140 self._pytype = pytype
141 else:
142 # Store the type name and defer loading of type
143 self._pytypeName = pytype
144 self._pytype = None
146 if components is not None:
147 if len(components) == 1: 147 ↛ 148line 147 didn't jump to line 148, because the condition on line 147 was never true
148 raise ValueError(
149 f"Composite storage class {name} is not allowed to have"
150 f" only one component '{next(iter(components))}'."
151 " Did you mean it to be a derived component?"
152 )
153 self._components = components
154 else:
155 self._components = {}
156 self._derivedComponents = derivedComponents if derivedComponents is not None else {}
157 self._parameters = frozenset(parameters) if parameters is not None else frozenset()
158 # if the delegate is not None also set it and clear the default
159 # delegate
160 self._delegate: type | None
161 self._delegateClassName: str | None
162 if delegate is not None:
163 self._delegateClassName = delegate
164 self._delegate = None
165 elif components is not None: 165 ↛ 168line 165 didn't jump to line 168, because the condition on line 165 was never true
166 # We set a default delegate for composites so that a class is
167 # guaranteed to support something if it is a composite.
168 log.debug("Setting default delegate for %s", self.name)
169 self._delegate = self.defaultDelegate
170 self._delegateClassName = self.defaultDelegateName
171 else:
172 self._delegate = None
173 self._delegateClassName = None
175 @property
176 def components(self) -> Mapping[str, StorageClass]:
177 """Return the components associated with this `StorageClass`."""
178 return self._components
180 @property
181 def derivedComponents(self) -> Mapping[str, StorageClass]:
182 """Return derived components associated with `StorageClass`."""
183 return self._derivedComponents
185 @property
186 def converters(self) -> Mapping[str, str]:
187 """Return the type converters supported by this `StorageClass`."""
188 return self._converters
190 def _get_converters_by_type(self) -> Mapping[type, Callable[[Any], Any]]:
191 """Return the type converters as python types."""
192 if self._converters_by_type is None:
193 self._converters_by_type = {}
195 # Loop over list because the dict can be edited in loop.
196 for candidate_type_str, converter_str in list(self.converters.items()):
197 if hasattr(builtins, candidate_type_str):
198 candidate_type = getattr(builtins, candidate_type_str)
199 else:
200 try:
201 candidate_type = doImportType(candidate_type_str)
202 except ImportError as e:
203 log.warning(
204 "Unable to import type %s associated with storage class %s (%s)",
205 candidate_type_str,
206 self.name,
207 e,
208 )
209 del self._converters[candidate_type_str]
210 continue
212 try:
213 converter = doImportType(converter_str)
214 except ImportError as e:
215 log.warning(
216 "Unable to import conversion function %s associated with storage class %s "
217 "required to convert type %s (%s)",
218 converter_str,
219 self.name,
220 candidate_type_str,
221 e,
222 )
223 del self._converters[candidate_type_str]
224 continue
225 if not callable(converter):
226 # doImportType is annotated to return a Type but in actual
227 # fact it can return Any except ModuleType because package
228 # variables can be accessed. This make mypy believe it
229 # is impossible for the return value to not be a callable
230 # so we must ignore the warning.
231 log.warning( # type: ignore
232 "Conversion function %s associated with storage class "
233 "%s to convert type %s is not a callable.",
234 converter_str,
235 self.name,
236 candidate_type_str,
237 )
238 del self._converters[candidate_type_str]
239 continue
240 self._converters_by_type[candidate_type] = converter
241 return self._converters_by_type
243 @property
244 def parameters(self) -> set[str]:
245 """Return `set` of names of supported parameters."""
246 return set(self._parameters)
248 @property
249 def pytype(self) -> type:
250 """Return Python type associated with this `StorageClass`."""
251 if self._pytype is not None:
252 return self._pytype
254 if hasattr(builtins, self._pytypeName):
255 pytype = getattr(builtins, self._pytypeName)
256 else:
257 pytype = doImportType(self._pytypeName)
258 self._pytype = pytype
259 return self._pytype
261 @property
262 def delegateClass(self) -> type | None:
263 """Class to use to delegate type-specific actions."""
264 if self._delegate is not None:
265 return self._delegate
266 if self._delegateClassName is None:
267 return None
268 delegate_class = doImportType(self._delegateClassName)
269 self._delegate = delegate_class
270 return self._delegate
272 def allComponents(self) -> Mapping[str, StorageClass]:
273 """Return all defined components.
275 This mapping includes all the derived and read/write components
276 for the corresponding storage class.
278 Returns
279 -------
280 comp : `dict` of [`str`, `StorageClass`]
281 The component name to storage class mapping.
282 """
283 return ChainMap(self._components, self._derivedComponents)
285 def delegate(self) -> StorageClassDelegate:
286 """Return an instance of a storage class delegate.
288 Returns
289 -------
290 delegate : `StorageClassDelegate`
291 Instance of the delegate associated with this `StorageClass`.
292 The delegate is constructed with this `StorageClass`.
294 Raises
295 ------
296 TypeError
297 This StorageClass has no associated delegate.
298 """
299 cls = self.delegateClass
300 if cls is None:
301 raise TypeError(f"No delegate class is associated with StorageClass {self.name}")
302 return cls(storageClass=self)
304 def isComposite(self) -> bool:
305 """Return Boolean indicating whether this is a composite or not.
307 Returns
308 -------
309 isComposite : `bool`
310 `True` if this `StorageClass` is a composite, `False`
311 otherwise.
312 """
313 if self.components:
314 return True
315 return False
317 def _lookupNames(self) -> tuple[LookupKey, ...]:
318 """Keys to use when looking up this DatasetRef in a configuration.
320 The names are returned in order of priority.
322 Returns
323 -------
324 names : `tuple` of `LookupKey`
325 Tuple of a `LookupKey` using the `StorageClass` name.
326 """
327 return (LookupKey(name=self.name),)
329 def knownParameters(self) -> set[str]:
330 """Return set of all parameters known to this `StorageClass`.
332 The set includes parameters understood by components of a composite.
334 Returns
335 -------
336 known : `set`
337 All parameter keys of this `StorageClass` and the component
338 storage classes.
339 """
340 known = set(self._parameters)
341 for sc in self.components.values():
342 known.update(sc.knownParameters())
343 return known
345 def validateParameters(self, parameters: Collection | None = None) -> None:
346 """Check that the parameters are known to this `StorageClass`.
348 Does not check the values.
350 Parameters
351 ----------
352 parameters : `~collections.abc.Collection`, optional
353 Collection containing the parameters. Can be `dict`-like or
354 `set`-like. The parameter values are not checked.
355 If no parameters are supplied, always returns without error.
357 Raises
358 ------
359 KeyError
360 Some parameters are not understood by this `StorageClass`.
361 """
362 # No parameters is always okay
363 if not parameters:
364 return
366 # Extract the important information into a set. Works for dict and
367 # list.
368 external = set(parameters)
370 diff = external - self.knownParameters()
371 if diff:
372 s = "s" if len(diff) > 1 else ""
373 unknown = "', '".join(diff)
374 raise KeyError(f"Parameter{s} '{unknown}' not understood by StorageClass {self.name}")
376 def filterParameters(
377 self, parameters: Mapping[str, Any] | None, subset: Collection | None = None
378 ) -> Mapping[str, Any]:
379 """Filter out parameters that are not known to this `StorageClass`.
381 Parameters
382 ----------
383 parameters : `~collections.abc.Mapping`, optional
384 Candidate parameters. Can be `None` if no parameters have
385 been provided.
386 subset : `~collections.abc.Collection`, optional
387 Subset of supported parameters that the caller is interested
388 in using. The subset must be known to the `StorageClass`
389 if specified. If `None` the supplied parameters will all
390 be checked, else only the keys in this set will be checked.
392 Returns
393 -------
394 filtered : `~collections.abc.Mapping`
395 Valid parameters. Empty `dict` if none are suitable.
397 Raises
398 ------
399 ValueError
400 Raised if the provided subset is not a subset of the supported
401 parameters or if it is an empty set.
402 """
403 if not parameters:
404 return {}
406 known = self.knownParameters()
408 if subset is not None:
409 if not subset:
410 raise ValueError("Specified a parameter subset but it was empty")
411 subset = set(subset)
412 if not subset.issubset(known):
413 raise ValueError(f"Requested subset ({subset}) is not a subset of known parameters ({known})")
414 wanted = subset
415 else:
416 wanted = known
418 return {k: parameters[k] for k in wanted if k in parameters}
420 def validateInstance(self, instance: Any) -> bool:
421 """Check that the supplied Python object has the expected Python type.
423 Parameters
424 ----------
425 instance : `object`
426 Object to check.
428 Returns
429 -------
430 isOk : `bool`
431 True if the supplied instance object can be handled by this
432 `StorageClass`, False otherwise.
433 """
434 return isinstance(instance, self.pytype)
436 def is_type(self, other: type, compare_types: bool = False) -> bool:
437 """Return Boolean indicating whether the supplied type matches
438 the type in this `StorageClass`.
440 Parameters
441 ----------
442 other : `type`
443 The type to be checked.
444 compare_types : `bool`, optional
445 If `True` the python type will be used in the comparison
446 if the type names do not match. This may trigger an import
447 of code and so can be slower.
449 Returns
450 -------
451 match : `bool`
452 `True` if the types are equal.
454 Notes
455 -----
456 If this `StorageClass` has not yet imported the Python type the
457 check is done against the full type name, this prevents an attempt
458 to import the type when it will likely not match.
459 """
460 if self._pytype:
461 return self._pytype is other
463 other_name = get_full_type_name(other)
464 if self._pytypeName == other_name:
465 return True
467 if compare_types:
468 # Must protect against the import failing.
469 try:
470 return self.pytype is other
471 except Exception:
472 pass
474 return False
476 def can_convert(self, other: StorageClass) -> bool:
477 """Return `True` if this storage class can convert python types
478 in the other storage class.
480 Parameters
481 ----------
482 other : `StorageClass`
483 The storage class to check.
485 Returns
486 -------
487 can : `bool`
488 `True` if this storage class has a registered converter for
489 the python type associated with the other storage class. That
490 converter will convert the other python type to the one associated
491 with this storage class.
492 """
493 if other.name == self.name:
494 # Identical storage classes are compatible.
495 return True
497 # It may be that the storage class being compared is not
498 # available because the python type can't be imported. In that
499 # case conversion must be impossible.
500 try:
501 other_pytype = other.pytype
502 except Exception:
503 return False
505 # Or even this storage class itself can not have the type imported.
506 try:
507 self_pytype = self.pytype
508 except Exception:
509 return False
511 if issubclass(other_pytype, self_pytype):
512 # Storage classes have different names but the same python type.
513 return True
515 for candidate_type in self._get_converters_by_type():
516 if issubclass(other_pytype, candidate_type):
517 return True
518 return False
520 def coerce_type(self, incorrect: Any) -> Any:
521 """Coerce the supplied incorrect instance to the python type
522 associated with this `StorageClass`.
524 Parameters
525 ----------
526 incorrect : `object`
527 An object that might be the incorrect type.
529 Returns
530 -------
531 correct : `object`
532 An object that matches the python type of this `StorageClass`.
533 Can be the same object as given. If `None`, `None` will be
534 returned.
536 Raises
537 ------
538 TypeError
539 Raised if no conversion can be found.
540 """
541 if incorrect is None:
542 return None
544 # Possible this is the correct type already.
545 if self.validateInstance(incorrect):
546 return incorrect
548 # Check each registered converter.
549 for candidate_type, converter in self._get_converters_by_type().items():
550 if isinstance(incorrect, candidate_type):
551 try:
552 return converter(incorrect)
553 except Exception:
554 log.error(
555 "Converter %s failed to convert type %s",
556 get_full_type_name(converter),
557 get_full_type_name(incorrect),
558 )
559 raise
560 raise TypeError(
561 "Type does not match and no valid converter found to convert"
562 f" '{get_full_type_name(incorrect)}' to '{get_full_type_name(self.pytype)}'"
563 )
565 def __eq__(self, other: Any) -> bool:
566 """Equality checks name, pytype name, delegate name, and components."""
567 if not isinstance(other, StorageClass):
568 return NotImplemented
570 if self.name != other.name:
571 return False
573 # We must compare pytype and delegate by name since we do not want
574 # to trigger an import of external module code here
575 if self._delegateClassName != other._delegateClassName:
576 return False
577 if self._pytypeName != other._pytypeName:
578 return False
580 # Ensure we have the same component keys in each
581 if set(self.components.keys()) != set(other.components.keys()):
582 return False
584 # Same parameters
585 if self.parameters != other.parameters:
586 return False
588 # Ensure that all the components have the same type
589 return all(self.components[k] == other.components[k] for k in self.components)
591 def __hash__(self) -> int:
592 return hash(self.name)
594 def __repr__(self) -> str:
595 optionals: dict[str, Any] = {}
596 if self._pytypeName != "object":
597 optionals["pytype"] = self._pytypeName
598 if self._delegateClassName is not None:
599 optionals["delegate"] = self._delegateClassName
600 if self._parameters:
601 optionals["parameters"] = self._parameters
602 if self.components:
603 optionals["components"] = self.components
604 if self.converters:
605 optionals["converters"] = self.converters
607 # order is preserved in the dict
608 options = ", ".join(f"{k}={v!r}" for k, v in optionals.items())
610 # Start with mandatory fields
611 r = f"{self.__class__.__name__}({self.name!r}"
612 if options:
613 r = r + ", " + options
614 r = r + ")"
615 return r
617 def __str__(self) -> str:
618 return self.name
621class StorageClassFactory(metaclass=Singleton):
622 """Factory for `StorageClass` instances.
624 This class is a singleton, with each instance sharing the pool of
625 StorageClasses. Since code can not know whether it is the first
626 time the instance has been created, the constructor takes no arguments.
627 To populate the factory with storage classes, a call to
628 `~StorageClassFactory.addFromConfig()` should be made.
630 Parameters
631 ----------
632 config : `StorageClassConfig` or `str`, optional
633 Load configuration. In a ButlerConfig` the relevant configuration
634 is located in the ``storageClasses`` section.
635 """
637 def __init__(self, config: StorageClassConfig | str | None = None):
638 self._storageClasses: dict[str, StorageClass] = {}
639 self._configs: list[StorageClassConfig] = []
641 # Always seed with the default config
642 self.addFromConfig(StorageClassConfig())
644 if config is not None: 644 ↛ 645line 644 didn't jump to line 645, because the condition on line 644 was never true
645 self.addFromConfig(config)
647 def __str__(self) -> str:
648 """Return summary of factory.
650 Returns
651 -------
652 summary : `str`
653 Summary of the factory status.
654 """
655 sep = "\n"
656 return f"""Number of registered StorageClasses: {len(self._storageClasses)}
658StorageClasses
659--------------
660{sep.join(f"{s}: {self._storageClasses[s]}" for s in self._storageClasses)}
661"""
663 def __contains__(self, storageClassOrName: StorageClass | str) -> bool:
664 """Indicate whether the storage class exists in the factory.
666 Parameters
667 ----------
668 storageClassOrName : `str` or `StorageClass`
669 If `str` is given existence of the named StorageClass
670 in the factory is checked. If `StorageClass` is given
671 existence and equality are checked.
673 Returns
674 -------
675 in : `bool`
676 True if the supplied string is present, or if the supplied
677 `StorageClass` is present and identical.
679 Notes
680 -----
681 The two different checks (one for "key" and one for "value") based on
682 the type of the given argument mean that it is possible for
683 StorageClass.name to be in the factory but StorageClass to not be
684 in the factory.
685 """
686 if isinstance(storageClassOrName, str): 686 ↛ 688line 686 didn't jump to line 688, because the condition on line 686 was never false
687 return storageClassOrName in self._storageClasses
688 elif isinstance(storageClassOrName, StorageClass) and storageClassOrName.name in self._storageClasses:
689 return storageClassOrName == self._storageClasses[storageClassOrName.name]
690 return False
692 def __len__(self) -> int:
693 return len(self._storageClasses)
695 def __iter__(self) -> Iterator[str]:
696 return iter(self._storageClasses)
698 def values(self) -> ValuesView[StorageClass]:
699 return self._storageClasses.values()
701 def keys(self) -> KeysView[str]:
702 return self._storageClasses.keys()
704 def items(self) -> ItemsView[str, StorageClass]:
705 return self._storageClasses.items()
707 def addFromConfig(self, config: StorageClassConfig | Config | str) -> None:
708 """Add more `StorageClass` definitions from a config file.
710 Parameters
711 ----------
712 config : `StorageClassConfig`, `Config` or `str`
713 Storage class configuration. Can contain a ``storageClasses``
714 key if part of a global configuration.
715 """
716 sconfig = StorageClassConfig(config)
717 self._configs.append(sconfig)
719 # Since we can not assume that we will get definitions of
720 # components or parents before their classes are defined
721 # we have a helper function that we can call recursively
722 # to extract definitions from the configuration.
723 def processStorageClass(name: str, sconfig: StorageClassConfig, msg: str = "") -> None:
724 # Maybe we've already processed this through recursion
725 if name not in sconfig:
726 return
727 info = sconfig.pop(name)
729 # Always create the storage class so we can ensure that
730 # we are not trying to overwrite with a different definition
731 components = None
733 # Extract scalar items from dict that are needed for
734 # StorageClass Constructor
735 storageClassKwargs = {k: info[k] for k in ("pytype", "delegate", "parameters") if k in info}
737 if "converters" in info:
738 storageClassKwargs["converters"] = info["converters"].toDict()
740 for compName in ("components", "derivedComponents"):
741 if compName not in info:
742 continue
743 components = {}
744 for cname, ctype in info[compName].items():
745 if ctype not in self:
746 processStorageClass(ctype, sconfig, msg)
747 components[cname] = self.getStorageClass(ctype)
749 # Fill in other items
750 storageClassKwargs[compName] = components
752 # Create the new storage class and register it
753 baseClass = None
754 if "inheritsFrom" in info:
755 baseName = info["inheritsFrom"]
756 if baseName not in self: 756 ↛ 757line 756 didn't jump to line 757, because the condition on line 756 was never true
757 processStorageClass(baseName, sconfig, msg)
758 baseClass = type(self.getStorageClass(baseName))
760 newStorageClassType = self.makeNewStorageClass(name, baseClass, **storageClassKwargs)
761 newStorageClass = newStorageClassType()
762 self.registerStorageClass(newStorageClass, msg=msg)
764 # In case there is a problem, construct a context message for any
765 # error reporting.
766 files = [str(f) for f in itertools.chain([sconfig.configFile], sconfig.filesRead) if f]
767 context = f"when adding definitions from {', '.join(files)}" if files else ""
768 log.debug("Adding definitions from config %s", ", ".join(files))
770 for name in list(sconfig.keys()):
771 processStorageClass(name, sconfig, context)
773 @staticmethod
774 def makeNewStorageClass(
775 name: str, baseClass: type[StorageClass] | None = StorageClass, **kwargs: Any
776 ) -> type[StorageClass]:
777 """Create a new Python class as a subclass of `StorageClass`.
779 Parameters
780 ----------
781 name : `str`
782 Name to use for this class.
783 baseClass : `type`, optional
784 Base class for this `StorageClass`. Must be either `StorageClass`
785 or a subclass of `StorageClass`. If `None`, `StorageClass` will
786 be used.
788 Returns
789 -------
790 newtype : `type` subclass of `StorageClass`
791 Newly created Python type.
792 """
793 if baseClass is None:
794 baseClass = StorageClass
795 if not issubclass(baseClass, StorageClass): 795 ↛ 796line 795 didn't jump to line 796, because the condition on line 795 was never true
796 raise ValueError(f"Base class must be a StorageClass not {baseClass}")
798 # convert the arguments to use different internal names
799 clsargs = {f"_cls_{k}": v for k, v in kwargs.items() if v is not None}
800 clsargs["_cls_name"] = name
802 # Some container items need to merge with the base class values
803 # so that a child can inherit but override one bit.
804 # lists (which you get from configs) are treated as sets for this to
805 # work consistently.
806 for k in ("components", "parameters", "derivedComponents", "converters"):
807 classKey = f"_cls_{k}"
808 if classKey in clsargs:
809 baseValue = getattr(baseClass, classKey, None)
810 if baseValue is not None:
811 currentValue = clsargs[classKey]
812 if isinstance(currentValue, dict): 812 ↛ 815line 812 didn't jump to line 815, because the condition on line 812 was never false
813 newValue = baseValue.copy()
814 else:
815 newValue = set(baseValue)
816 newValue.update(currentValue)
817 clsargs[classKey] = newValue
819 # If we have parameters they should be a frozen set so that the
820 # parameters in the class can not be modified.
821 pk = "_cls_parameters"
822 if pk in clsargs:
823 clsargs[pk] = frozenset(clsargs[pk])
825 return type(f"StorageClass{name}", (baseClass,), clsargs)
827 def getStorageClass(self, storageClassName: str) -> StorageClass:
828 """Get a StorageClass instance associated with the supplied name.
830 Parameters
831 ----------
832 storageClassName : `str`
833 Name of the storage class to retrieve.
835 Returns
836 -------
837 instance : `StorageClass`
838 Instance of the correct `StorageClass`.
840 Raises
841 ------
842 KeyError
843 The requested storage class name is not registered.
844 """
845 return self._storageClasses[storageClassName]
847 def findStorageClass(self, pytype: type, compare_types: bool = False) -> StorageClass:
848 """Find the storage class associated with this python type.
850 Parameters
851 ----------
852 pytype : `type`
853 The Python type to be matched.
854 compare_types : `bool`, optional
855 If `False`, the type will be checked against name of the python
856 type. This comparison is always done first. If `True` and the
857 string comparison failed, each candidate storage class will be
858 forced to have its type imported. This can be significantly slower.
860 Returns
861 -------
862 storageClass : `StorageClass`
863 The matching storage class.
865 Raises
866 ------
867 KeyError
868 Raised if no match could be found.
870 Notes
871 -----
872 It is possible for a python type to be associated with multiple
873 storage classes. This method will currently return the first that
874 matches.
875 """
876 result = self._find_storage_class(pytype, False)
877 if result:
878 return result
880 if compare_types:
881 # The fast comparison failed and we were asked to try the
882 # variant that might involve code imports.
883 result = self._find_storage_class(pytype, True)
884 if result:
885 return result
887 raise KeyError(f"Unable to find a StorageClass associated with type {get_full_type_name(pytype)!r}")
889 def _find_storage_class(self, pytype: type, compare_types: bool) -> StorageClass | None:
890 """Iterate through all storage classes to find a match.
892 Parameters
893 ----------
894 pytype : `type`
895 The Python type to be matched.
896 compare_types : `bool`, optional
897 Whether to use type name matching or explicit type matching.
898 The latter can be slower.
900 Returns
901 -------
902 storageClass : `StorageClass` or `None`
903 The matching storage class, or `None` if no match was found.
905 Notes
906 -----
907 Helper method for ``findStorageClass``.
908 """
909 for storageClass in self.values():
910 if storageClass.is_type(pytype, compare_types=compare_types):
911 return storageClass
912 return None
914 def registerStorageClass(self, storageClass: StorageClass, msg: str | None = None) -> None:
915 """Store the `StorageClass` in the factory.
917 Will be indexed by `StorageClass.name` and will return instances
918 of the supplied `StorageClass`.
920 Parameters
921 ----------
922 storageClass : `StorageClass`
923 Type of the Python `StorageClass` to register.
924 msg : `str`, optional
925 Additional message string to be included in any error message.
927 Raises
928 ------
929 ValueError
930 If a storage class has already been registered with
931 that storage class name and the previous definition differs.
932 """
933 if storageClass.name in self._storageClasses: 933 ↛ 934line 933 didn't jump to line 934, because the condition on line 933 was never true
934 existing = self.getStorageClass(storageClass.name)
935 if existing != storageClass:
936 errmsg = f" {msg}" if msg else ""
937 raise ValueError(
938 f"New definition for StorageClass {storageClass.name} ({storageClass!r}) "
939 f"differs from current definition ({existing!r}){errmsg}"
940 )
941 else:
942 self._storageClasses[storageClass.name] = storageClass
944 def _unregisterStorageClass(self, storageClassName: str) -> None:
945 """Remove the named StorageClass from the factory.
947 Parameters
948 ----------
949 storageClassName : `str`
950 Name of storage class to remove.
952 Raises
953 ------
954 KeyError
955 The named storage class is not registered.
957 Notes
958 -----
959 This method is intended to simplify testing of StorageClassFactory
960 functionality and it is not expected to be required for normal usage.
961 """
962 del self._storageClasses[storageClassName]