Coverage for python/lsst/daf/butler/core/storageClass.py: 46%
327 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 14:18 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-08 14:18 -0800
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 Any, Collection, Dict, List, Mapping, Optional, Sequence, Set, Tuple, Type, Union
34from lsst.utils import doImportType
35from lsst.utils.classes import Singleton
36from lsst.utils.introspection import get_full_type_name
38from .config import Config, ConfigSubset
39from .configSupport import LookupKey
40from .storageClassDelegate import StorageClassDelegate
42log = logging.getLogger(__name__)
45class StorageClassConfig(ConfigSubset):
46 """Configuration class for defining Storage Classes."""
48 component = "storageClasses"
49 defaultConfigFile = "storageClasses.yaml"
52class StorageClass:
53 """Class describing how a label maps to a particular Python type.
55 Parameters
56 ----------
57 name : `str`
58 Name to use for this class.
59 pytype : `type` or `str`
60 Python type (or name of type) to associate with the `StorageClass`
61 components : `dict`, optional
62 `dict` mapping name of a component to another `StorageClass`.
63 derivedComponents : `dict`, optional
64 `dict` mapping name of a derived component to another `StorageClass`.
65 parameters : `~collections.abc.Sequence` or `~collections.abc.Set`
66 Parameters understood by this `StorageClass` that can control
67 reading of data from datastores.
68 delegate : `str`, optional
69 Fully qualified name of class supporting assembly and disassembly
70 of a `pytype` instance.
71 converters : `dict` [`str`, `str`], optional
72 Mapping of python type to function that can be called to convert
73 that python type to the valid type of this storage class.
74 """
76 _cls_name: str = "BaseStorageClass"
77 _cls_components: Optional[Dict[str, StorageClass]] = None
78 _cls_derivedComponents: Optional[Dict[str, StorageClass]] = None
79 _cls_parameters: Optional[Union[Set[str], Sequence[str]]] = None
80 _cls_delegate: Optional[str] = None
81 _cls_pytype: Optional[Union[Type, str]] = None
82 _cls_converters: Optional[Dict[str, str]] = None
83 defaultDelegate: Type = StorageClassDelegate
84 defaultDelegateName: str = get_full_type_name(defaultDelegate)
86 def __init__(
87 self,
88 name: Optional[str] = None,
89 pytype: Optional[Union[Type, str]] = None,
90 components: Optional[Dict[str, StorageClass]] = None,
91 derivedComponents: Optional[Dict[str, StorageClass]] = None,
92 parameters: Optional[Union[Sequence, Set]] = None,
93 delegate: Optional[str] = None,
94 converters: Optional[Dict[str, str]] = None,
95 ):
96 if name is None:
97 name = self._cls_name
98 if pytype is None: 98 ↛ 100line 98 didn't jump to line 100, because the condition on line 98 was never false
99 pytype = self._cls_pytype
100 if components is None: 100 ↛ 102line 100 didn't jump to line 102, because the condition on line 100 was never false
101 components = self._cls_components
102 if derivedComponents is None: 102 ↛ 104line 102 didn't jump to line 104, because the condition on line 102 was never false
103 derivedComponents = self._cls_derivedComponents
104 if parameters is None: 104 ↛ 106line 104 didn't jump to line 106, because the condition on line 104 was never false
105 parameters = self._cls_parameters
106 if delegate is None: 106 ↛ 110line 106 didn't jump to line 110, because the condition on line 106 was never false
107 delegate = self._cls_delegate
109 # Merge converters with class defaults.
110 self._converters = {}
111 if self._cls_converters is not None:
112 self._converters.update(self._cls_converters)
113 if converters: 113 ↛ 114line 113 didn't jump to line 114, because the condition on line 113 was never true
114 self._converters.update(converters)
116 # Version of converters where the python types have been
117 # Do not try to import anything until needed.
118 self._converters_by_type: Optional[Dict[Type, Type]] = None
120 self.name = name
122 if pytype is None:
123 pytype = object
125 self._pytype: Optional[Type]
126 if not isinstance(pytype, str):
127 # Already have a type so store it and get the name
128 self._pytypeName = get_full_type_name(pytype)
129 self._pytype = pytype
130 else:
131 # Store the type name and defer loading of type
132 self._pytypeName = pytype
133 self._pytype = None
135 if components is not None:
136 if len(components) == 1: 136 ↛ 137line 136 didn't jump to line 137, because the condition on line 136 was never true
137 raise ValueError(
138 f"Composite storage class {name} is not allowed to have"
139 f" only one component '{next(iter(components))}'."
140 " Did you mean it to be a derived component?"
141 )
142 self._components = components
143 else:
144 self._components = {}
145 self._derivedComponents = derivedComponents if derivedComponents is not None else {}
146 self._parameters = frozenset(parameters) if parameters is not None else frozenset()
147 # if the delegate is not None also set it and clear the default
148 # delegate
149 self._delegate: Optional[Type]
150 self._delegateClassName: Optional[str]
151 if delegate is not None:
152 self._delegateClassName = delegate
153 self._delegate = None
154 elif components is not None: 154 ↛ 157line 154 didn't jump to line 157, because the condition on line 154 was never true
155 # We set a default delegate for composites so that a class is
156 # guaranteed to support something if it is a composite.
157 log.debug("Setting default delegate for %s", self.name)
158 self._delegate = self.defaultDelegate
159 self._delegateClassName = self.defaultDelegateName
160 else:
161 self._delegate = None
162 self._delegateClassName = None
164 @property
165 def components(self) -> Dict[str, StorageClass]:
166 """Return the components associated with this `StorageClass`."""
167 return self._components
169 @property
170 def derivedComponents(self) -> Dict[str, StorageClass]:
171 """Return derived components associated with `StorageClass`."""
172 return self._derivedComponents
174 @property
175 def converters(self) -> Dict[str, str]:
176 """Return the type converters supported by this `StorageClass`."""
177 return self._converters
179 @property
180 def converters_by_type(self) -> Dict[Type, Type]:
181 """Return the type converters as python types."""
182 if self._converters_by_type is None:
183 self._converters_by_type = {}
185 # Loop over list because the dict can be edited in loop.
186 for candidate_type_str, converter_str in list(self.converters.items()):
187 if hasattr(builtins, candidate_type_str):
188 candidate_type = getattr(builtins, candidate_type_str)
189 else:
190 try:
191 candidate_type = doImportType(candidate_type_str)
192 except ImportError as e:
193 log.warning(
194 "Unable to import type %s associated with storage class %s (%s)",
195 candidate_type_str,
196 self.name,
197 e,
198 )
199 del self.converters[candidate_type_str]
200 continue
202 try:
203 converter = doImportType(converter_str)
204 except ImportError as e:
205 log.warning(
206 "Unable to import conversion function %s associated with storage class %s "
207 "required to convert type %s (%s)",
208 converter_str,
209 self.name,
210 candidate_type_str,
211 e,
212 )
213 del self.converters[candidate_type_str]
214 continue
215 if not callable(converter):
216 # doImportType is annotated to return a Type but in actual
217 # fact it can return Any except ModuleType because package
218 # variables can be accessed. This make mypy believe it
219 # is impossible for the return value to not be a callable
220 # so we must ignore the warning.
221 log.warning( # type: ignore
222 "Conversion function %s associated with storage class "
223 "%s to convert type %s is not a callable.",
224 converter_str,
225 self.name,
226 candidate_type_str,
227 )
228 del self.converters[candidate_type_str]
229 continue
230 self._converters_by_type[candidate_type] = converter
231 return self._converters_by_type
233 @property
234 def parameters(self) -> Set[str]:
235 """Return `set` of names of supported parameters."""
236 return set(self._parameters)
238 @property
239 def pytype(self) -> Type:
240 """Return Python type associated with this `StorageClass`."""
241 if self._pytype is not None:
242 return self._pytype
244 if hasattr(builtins, self._pytypeName):
245 pytype = getattr(builtins, self._pytypeName)
246 else:
247 pytype = doImportType(self._pytypeName)
248 self._pytype = pytype
249 return self._pytype
251 @property
252 def delegateClass(self) -> Optional[Type]:
253 """Class to use to delegate type-specific actions."""
254 if self._delegate is not None:
255 return self._delegate
256 if self._delegateClassName is None:
257 return None
258 delegate_class = doImportType(self._delegateClassName)
259 self._delegate = delegate_class
260 return self._delegate
262 def allComponents(self) -> Mapping[str, StorageClass]:
263 """Return all defined components.
265 This mapping includes all the derived and read/write components
266 for the corresponding storage class.
268 Returns
269 -------
270 comp : `dict` of [`str`, `StorageClass`]
271 The component name to storage class mapping.
272 """
273 components = copy.copy(self.components)
274 components.update(self.derivedComponents)
275 return components
277 def delegate(self) -> StorageClassDelegate:
278 """Return an instance of a storage class delegate.
280 Returns
281 -------
282 delegate : `StorageClassDelegate`
283 Instance of the delegate associated with this `StorageClass`.
284 The delegate is constructed with this `StorageClass`.
286 Raises
287 ------
288 TypeError
289 This StorageClass has no associated delegate.
290 """
291 cls = self.delegateClass
292 if cls is None:
293 raise TypeError(f"No delegate class is associated with StorageClass {self.name}")
294 return cls(storageClass=self)
296 def isComposite(self) -> bool:
297 """Return Boolean indicating whether this is a composite or not.
299 Returns
300 -------
301 isComposite : `bool`
302 `True` if this `StorageClass` is a composite, `False`
303 otherwise.
304 """
305 if self.components:
306 return True
307 return False
309 def _lookupNames(self) -> Tuple[LookupKey, ...]:
310 """Keys to use when looking up this DatasetRef in a configuration.
312 The names are returned in order of priority.
314 Returns
315 -------
316 names : `tuple` of `LookupKey`
317 Tuple of a `LookupKey` using the `StorageClass` name.
318 """
319 return (LookupKey(name=self.name),)
321 def knownParameters(self) -> Set[str]:
322 """Return set of all parameters known to this `StorageClass`.
324 The set includes parameters understood by components of a composite.
326 Returns
327 -------
328 known : `set`
329 All parameter keys of this `StorageClass` and the component
330 storage classes.
331 """
332 known = set(self._parameters)
333 for sc in self.components.values():
334 known.update(sc.knownParameters())
335 return known
337 def validateParameters(self, parameters: Collection = None) -> None:
338 """Check that the parameters are known to this `StorageClass`.
340 Does not check the values.
342 Parameters
343 ----------
344 parameters : `~collections.abc.Collection`, optional
345 Collection containing the parameters. Can be `dict`-like or
346 `set`-like. The parameter values are not checked.
347 If no parameters are supplied, always returns without error.
349 Raises
350 ------
351 KeyError
352 Some parameters are not understood by this `StorageClass`.
353 """
354 # No parameters is always okay
355 if not parameters:
356 return
358 # Extract the important information into a set. Works for dict and
359 # list.
360 external = set(parameters)
362 diff = external - self.knownParameters()
363 if diff:
364 s = "s" if len(diff) > 1 else ""
365 unknown = "', '".join(diff)
366 raise KeyError(f"Parameter{s} '{unknown}' not understood by StorageClass {self.name}")
368 def filterParameters(self, parameters: Mapping[str, Any], subset: Collection = None) -> Mapping[str, Any]:
369 """Filter out parameters that are not known to this `StorageClass`.
371 Parameters
372 ----------
373 parameters : `Mapping`, optional
374 Candidate parameters. Can be `None` if no parameters have
375 been provided.
376 subset : `~collections.abc.Collection`, optional
377 Subset of supported parameters that the caller is interested
378 in using. The subset must be known to the `StorageClass`
379 if specified. If `None` the supplied parameters will all
380 be checked, else only the keys in this set will be checked.
382 Returns
383 -------
384 filtered : `Mapping`
385 Valid parameters. Empty `dict` if none are suitable.
387 Raises
388 ------
389 ValueError
390 Raised if the provided subset is not a subset of the supported
391 parameters or if it is an empty set.
392 """
393 if not parameters:
394 return {}
396 known = self.knownParameters()
398 if subset is not None:
399 if not subset:
400 raise ValueError("Specified a parameter subset but it was empty")
401 subset = set(subset)
402 if not subset.issubset(known):
403 raise ValueError(f"Requested subset ({subset}) is not a subset of known parameters ({known})")
404 wanted = subset
405 else:
406 wanted = known
408 return {k: parameters[k] for k in wanted if k in parameters}
410 def validateInstance(self, instance: Any) -> bool:
411 """Check that the supplied Python object has the expected Python type.
413 Parameters
414 ----------
415 instance : `object`
416 Object to check.
418 Returns
419 -------
420 isOk : `bool`
421 True if the supplied instance object can be handled by this
422 `StorageClass`, False otherwise.
423 """
424 return isinstance(instance, self.pytype)
426 def can_convert(self, other: StorageClass) -> bool:
427 """Return `True` if this storage class can convert python types
428 in the other storage class.
430 Parameters
431 ----------
432 other : `StorageClass`
433 The storage class to check.
435 Returns
436 -------
437 can : `bool`
438 `True` if this storage class has a registered converter for
439 the python type associated with the other storage class. That
440 converter will convert the other python type to the one associated
441 with this storage class.
442 """
443 if other.name == self.name:
444 # Identical storage classes are compatible.
445 return True
447 # It may be that the storage class being compared is not
448 # available because the python type can't be imported. In that
449 # case conversion must be impossible.
450 try:
451 other_pytype = other.pytype
452 except Exception:
453 return False
455 # Or even this storage class itself can not have the type imported.
456 try:
457 self_pytype = self.pytype
458 except Exception:
459 return False
461 if issubclass(other_pytype, self_pytype):
462 # Storage classes have different names but the same python type.
463 return True
465 for candidate_type in self.converters_by_type:
466 if issubclass(other_pytype, candidate_type):
467 return True
468 return False
470 def coerce_type(self, incorrect: Any) -> Any:
471 """Coerce the supplied incorrect instance to the python type
472 associated with this `StorageClass`.
474 Parameters
475 ----------
476 incorrect : `object`
477 An object that might be the incorrect type.
479 Returns
480 -------
481 correct : `object`
482 An object that matches the python type of this `StorageClass`.
483 Can be the same object as given. If `None`, `None` will be
484 returned.
486 Raises
487 ------
488 TypeError
489 Raised if no conversion can be found.
490 """
491 if incorrect is None:
492 return None
494 # Possible this is the correct type already.
495 if self.validateInstance(incorrect):
496 return incorrect
498 # Check each registered converter.
499 for candidate_type, converter in self.converters_by_type.items():
500 if isinstance(incorrect, candidate_type):
501 try:
502 return converter(incorrect)
503 except Exception:
504 log.error(
505 "Converter %s failed to convert type %s",
506 get_full_type_name(converter),
507 get_full_type_name(incorrect),
508 )
509 raise
510 raise TypeError(
511 "Type does not match and no valid converter found to convert"
512 f" '{type(incorrect)}' to '{self.pytype}'"
513 )
515 def __eq__(self, other: Any) -> bool:
516 """Equality checks name, pytype name, delegate name, and components."""
517 if not isinstance(other, StorageClass):
518 return NotImplemented
520 if self.name != other.name:
521 return False
523 # We must compare pytype and delegate by name since we do not want
524 # to trigger an import of external module code here
525 if self._delegateClassName != other._delegateClassName:
526 return False
527 if self._pytypeName != other._pytypeName:
528 return False
530 # Ensure we have the same component keys in each
531 if set(self.components.keys()) != set(other.components.keys()):
532 return False
534 # Same parameters
535 if self.parameters != other.parameters:
536 return False
538 # Ensure that all the components have the same type
539 for k in self.components:
540 if self.components[k] != other.components[k]:
541 return False
543 # If we got to this point everything checks out
544 return True
546 def __hash__(self) -> int:
547 return hash(self.name)
549 def __repr__(self) -> str:
550 optionals: Dict[str, Any] = {}
551 if self._pytypeName != "object":
552 optionals["pytype"] = self._pytypeName
553 if self._delegateClassName is not None:
554 optionals["delegate"] = self._delegateClassName
555 if self._parameters:
556 optionals["parameters"] = self._parameters
557 if self.components:
558 optionals["components"] = self.components
559 if self.converters:
560 optionals["converters"] = self.converters
562 # order is preserved in the dict
563 options = ", ".join(f"{k}={v!r}" for k, v in optionals.items())
565 # Start with mandatory fields
566 r = f"{self.__class__.__name__}({self.name!r}"
567 if options:
568 r = r + ", " + options
569 r = r + ")"
570 return r
572 def __str__(self) -> str:
573 return self.name
576class StorageClassFactory(metaclass=Singleton):
577 """Factory for `StorageClass` instances.
579 This class is a singleton, with each instance sharing the pool of
580 StorageClasses. Since code can not know whether it is the first
581 time the instance has been created, the constructor takes no arguments.
582 To populate the factory with storage classes, a call to
583 `~StorageClassFactory.addFromConfig()` should be made.
585 Parameters
586 ----------
587 config : `StorageClassConfig` or `str`, optional
588 Load configuration. In a ButlerConfig` the relevant configuration
589 is located in the ``storageClasses`` section.
590 """
592 def __init__(self, config: Optional[Union[StorageClassConfig, str]] = None):
593 self._storageClasses: Dict[str, StorageClass] = {}
594 self._configs: List[StorageClassConfig] = []
596 # Always seed with the default config
597 self.addFromConfig(StorageClassConfig())
599 if config is not None: 599 ↛ 600line 599 didn't jump to line 600, because the condition on line 599 was never true
600 self.addFromConfig(config)
602 def __str__(self) -> str:
603 """Return summary of factory.
605 Returns
606 -------
607 summary : `str`
608 Summary of the factory status.
609 """
610 sep = "\n"
611 return f"""Number of registered StorageClasses: {len(self._storageClasses)}
613StorageClasses
614--------------
615{sep.join(f"{s}: {self._storageClasses[s]}" for s in self._storageClasses)}
616"""
618 def __contains__(self, storageClassOrName: Union[StorageClass, str]) -> bool:
619 """Indicate whether the storage class exists in the factory.
621 Parameters
622 ----------
623 storageClassOrName : `str` or `StorageClass`
624 If `str` is given existence of the named StorageClass
625 in the factory is checked. If `StorageClass` is given
626 existence and equality are checked.
628 Returns
629 -------
630 in : `bool`
631 True if the supplied string is present, or if the supplied
632 `StorageClass` is present and identical.
634 Notes
635 -----
636 The two different checks (one for "key" and one for "value") based on
637 the type of the given argument mean that it is possible for
638 StorageClass.name to be in the factory but StorageClass to not be
639 in the factory.
640 """
641 if isinstance(storageClassOrName, str): 641 ↛ 643line 641 didn't jump to line 643, because the condition on line 641 was never false
642 return storageClassOrName in self._storageClasses
643 elif isinstance(storageClassOrName, StorageClass):
644 if storageClassOrName.name in self._storageClasses:
645 return storageClassOrName == self._storageClasses[storageClassOrName.name]
646 return False
648 def addFromConfig(self, config: Union[StorageClassConfig, Config, str]) -> None:
649 """Add more `StorageClass` definitions from a config file.
651 Parameters
652 ----------
653 config : `StorageClassConfig`, `Config` or `str`
654 Storage class configuration. Can contain a ``storageClasses``
655 key if part of a global configuration.
656 """
657 sconfig = StorageClassConfig(config)
658 self._configs.append(sconfig)
660 # Since we can not assume that we will get definitions of
661 # components or parents before their classes are defined
662 # we have a helper function that we can call recursively
663 # to extract definitions from the configuration.
664 def processStorageClass(name: str, sconfig: StorageClassConfig, msg: str = "") -> None:
665 # Maybe we've already processed this through recursion
666 if name not in sconfig:
667 return
668 info = sconfig.pop(name)
670 # Always create the storage class so we can ensure that
671 # we are not trying to overwrite with a different definition
672 components = None
674 # Extract scalar items from dict that are needed for
675 # StorageClass Constructor
676 storageClassKwargs = {k: info[k] for k in ("pytype", "delegate", "parameters") if k in info}
678 if "converters" in info:
679 storageClassKwargs["converters"] = info["converters"].toDict()
681 for compName in ("components", "derivedComponents"):
682 if compName not in info:
683 continue
684 components = {}
685 for cname, ctype in info[compName].items():
686 if ctype not in self:
687 processStorageClass(ctype, sconfig, msg)
688 components[cname] = self.getStorageClass(ctype)
690 # Fill in other items
691 storageClassKwargs[compName] = components
693 # Create the new storage class and register it
694 baseClass = None
695 if "inheritsFrom" in info:
696 baseName = info["inheritsFrom"]
697 if baseName not in self: 697 ↛ 698line 697 didn't jump to line 698, because the condition on line 697 was never true
698 processStorageClass(baseName, sconfig, msg)
699 baseClass = type(self.getStorageClass(baseName))
701 newStorageClassType = self.makeNewStorageClass(name, baseClass, **storageClassKwargs)
702 newStorageClass = newStorageClassType()
703 self.registerStorageClass(newStorageClass, msg=msg)
705 # In case there is a problem, construct a context message for any
706 # error reporting.
707 files = [str(f) for f in itertools.chain([sconfig.configFile], sconfig.filesRead) if f]
708 context = f"when adding definitions from {', '.join(files)}" if files else ""
709 log.debug("Adding definitions from config %s", ", ".join(files))
711 for name in list(sconfig.keys()):
712 processStorageClass(name, sconfig, context)
714 @staticmethod
715 def makeNewStorageClass(
716 name: str, baseClass: Optional[Type[StorageClass]] = StorageClass, **kwargs: Any
717 ) -> Type[StorageClass]:
718 """Create a new Python class as a subclass of `StorageClass`.
720 Parameters
721 ----------
722 name : `str`
723 Name to use for this class.
724 baseClass : `type`, optional
725 Base class for this `StorageClass`. Must be either `StorageClass`
726 or a subclass of `StorageClass`. If `None`, `StorageClass` will
727 be used.
729 Returns
730 -------
731 newtype : `type` subclass of `StorageClass`
732 Newly created Python type.
733 """
734 if baseClass is None:
735 baseClass = StorageClass
736 if not issubclass(baseClass, StorageClass): 736 ↛ 737line 736 didn't jump to line 737, because the condition on line 736 was never true
737 raise ValueError(f"Base class must be a StorageClass not {baseClass}")
739 # convert the arguments to use different internal names
740 clsargs = {f"_cls_{k}": v for k, v in kwargs.items() if v is not None}
741 clsargs["_cls_name"] = name
743 # Some container items need to merge with the base class values
744 # so that a child can inherit but override one bit.
745 # lists (which you get from configs) are treated as sets for this to
746 # work consistently.
747 for k in ("components", "parameters", "derivedComponents", "converters"):
748 classKey = f"_cls_{k}"
749 if classKey in clsargs:
750 baseValue = getattr(baseClass, classKey, None)
751 if baseValue is not None:
752 currentValue = clsargs[classKey]
753 if isinstance(currentValue, dict): 753 ↛ 756line 753 didn't jump to line 756, because the condition on line 753 was never false
754 newValue = baseValue.copy()
755 else:
756 newValue = set(baseValue)
757 newValue.update(currentValue)
758 clsargs[classKey] = newValue
760 # If we have parameters they should be a frozen set so that the
761 # parameters in the class can not be modified.
762 pk = "_cls_parameters"
763 if pk in clsargs:
764 clsargs[pk] = frozenset(clsargs[pk])
766 return type(f"StorageClass{name}", (baseClass,), clsargs)
768 def getStorageClass(self, storageClassName: str) -> StorageClass:
769 """Get a StorageClass instance associated with the supplied name.
771 Parameters
772 ----------
773 storageClassName : `str`
774 Name of the storage class to retrieve.
776 Returns
777 -------
778 instance : `StorageClass`
779 Instance of the correct `StorageClass`.
781 Raises
782 ------
783 KeyError
784 The requested storage class name is not registered.
785 """
786 return self._storageClasses[storageClassName]
788 def registerStorageClass(self, storageClass: StorageClass, msg: Optional[str] = None) -> None:
789 """Store the `StorageClass` in the factory.
791 Will be indexed by `StorageClass.name` and will return instances
792 of the supplied `StorageClass`.
794 Parameters
795 ----------
796 storageClass : `StorageClass`
797 Type of the Python `StorageClass` to register.
798 msg : `str`, optional
799 Additional message string to be included in any error message.
801 Raises
802 ------
803 ValueError
804 If a storage class has already been registered with
805 that storage class name and the previous definition differs.
806 """
807 if storageClass.name in self._storageClasses: 807 ↛ 808line 807 didn't jump to line 808, because the condition on line 807 was never true
808 existing = self.getStorageClass(storageClass.name)
809 if existing != storageClass:
810 errmsg = f" {msg}" if msg else ""
811 raise ValueError(
812 f"New definition for StorageClass {storageClass.name} ({storageClass!r}) "
813 f"differs from current definition ({existing!r}){errmsg}"
814 )
815 else:
816 self._storageClasses[storageClass.name] = storageClass
818 def _unregisterStorageClass(self, storageClassName: str) -> None:
819 """Remove the named StorageClass from the factory.
821 Parameters
822 ----------
823 storageClassName : `str`
824 Name of storage class to remove.
826 Raises
827 ------
828 KeyError
829 The named storage class is not registered.
831 Notes
832 -----
833 This method is intended to simplify testing of StorageClassFactory
834 functionality and it is not expected to be required for normal usage.
835 """
836 del self._storageClasses[storageClassName]