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