Coverage for python/lsst/daf/butler/core/storageClass.py: 47%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22from __future__ import annotations
24"""Support for Storage Classes."""
26__all__ = ("StorageClass", "StorageClassFactory", "StorageClassConfig")
28import builtins
29import copy
30import logging
31from typing import Any, Collection, Dict, List, Mapping, Optional, Sequence, Set, Tuple, Type, Union
33from lsst.utils import doImportType
34from lsst.utils.classes import Singleton
35from lsst.utils.introspection import get_full_type_name
37from .config import Config, ConfigSubset
38from .configSupport import LookupKey
39from .storageClassDelegate import StorageClassDelegate
41log = logging.getLogger(__name__)
44class StorageClassConfig(ConfigSubset):
45 """Configuration class for defining Storage Classes."""
47 component = "storageClasses"
48 defaultConfigFile = "storageClasses.yaml"
51class StorageClass:
52 """Class describing how a label maps to a particular Python type.
54 Parameters
55 ----------
56 name : `str`
57 Name to use for this class.
58 pytype : `type` or `str`
59 Python type (or name of type) to associate with the `StorageClass`
60 components : `dict`, optional
61 `dict` mapping name of a component to another `StorageClass`.
62 derivedComponents : `dict`, optional
63 `dict` mapping name of a derived component to another `StorageClass`.
64 parameters : `~collections.abc.Sequence` or `~collections.abc.Set`
65 Parameters understood by this `StorageClass` that can control
66 reading of data from datastores.
67 delegate : `str`, optional
68 Fully qualified name of class supporting assembly and disassembly
69 of a `pytype` instance.
70 converters : `dict` [`str`, `str`], optional
71 Mapping of python type to function that can be called to convert
72 that python type to the valid type of this storage class.
73 """
75 _cls_name: str = "BaseStorageClass"
76 _cls_components: Optional[Dict[str, StorageClass]] = None
77 _cls_derivedComponents: Optional[Dict[str, StorageClass]] = None
78 _cls_parameters: Optional[Union[Set[str], Sequence[str]]] = None
79 _cls_delegate: Optional[str] = None
80 _cls_pytype: Optional[Union[Type, str]] = None
81 _cls_converters: Optional[Dict[str, str]] = None
82 defaultDelegate: Type = StorageClassDelegate
83 defaultDelegateName: str = get_full_type_name(defaultDelegate)
85 def __init__(
86 self,
87 name: Optional[str] = None,
88 pytype: Optional[Union[Type, str]] = None,
89 components: Optional[Dict[str, StorageClass]] = None,
90 derivedComponents: Optional[Dict[str, StorageClass]] = None,
91 parameters: Optional[Union[Sequence, Set]] = None,
92 delegate: Optional[str] = None,
93 converters: Optional[Dict[str, str]] = None,
94 ):
95 if name is None:
96 name = self._cls_name
97 if pytype is None: 97 ↛ 99line 97 didn't jump to line 99, because the condition on line 97 was never false
98 pytype = self._cls_pytype
99 if components is None: 99 ↛ 101line 99 didn't jump to line 101, because the condition on line 99 was never false
100 components = self._cls_components
101 if derivedComponents is None: 101 ↛ 103line 101 didn't jump to line 103, because the condition on line 101 was never false
102 derivedComponents = self._cls_derivedComponents
103 if parameters is None: 103 ↛ 105line 103 didn't jump to line 105, because the condition on line 103 was never false
104 parameters = self._cls_parameters
105 if delegate is None: 105 ↛ 109line 105 didn't jump to line 109, because the condition on line 105 was never false
106 delegate = self._cls_delegate
108 # Merge converters with class defaults.
109 self._converters = {}
110 if self._cls_converters is not None:
111 self._converters.update(self._cls_converters)
112 if converters: 112 ↛ 113line 112 didn't jump to line 113, because the condition on line 112 was never true
113 self._converters.update(converters)
115 # Version of converters where the python types have been
116 # Do not try to import anything until needed.
117 self._converters_by_type: Optional[Dict[Type, Type]] = None
119 self.name = name
121 if pytype is None:
122 pytype = object
124 self._pytype: Optional[Type]
125 if not isinstance(pytype, str):
126 # Already have a type so store it and get the name
127 self._pytypeName = get_full_type_name(pytype)
128 self._pytype = pytype
129 else:
130 # Store the type name and defer loading of type
131 self._pytypeName = pytype
132 self._pytype = None
134 if components is not None:
135 if len(components) == 1: 135 ↛ 136line 135 didn't jump to line 136, because the condition on line 135 was never true
136 raise ValueError(
137 f"Composite storage class {name} is not allowed to have"
138 f" only one component '{next(iter(components))}'."
139 " Did you mean it to be a derived component?"
140 )
141 self._components = components
142 else:
143 self._components = {}
144 self._derivedComponents = derivedComponents if derivedComponents is not None else {}
145 self._parameters = frozenset(parameters) if parameters is not None else frozenset()
146 # if the delegate is not None also set it and clear the default
147 # delegate
148 self._delegate: Optional[Type]
149 self._delegateClassName: Optional[str]
150 if delegate is not None:
151 self._delegateClassName = delegate
152 self._delegate = None
153 elif components is not None: 153 ↛ 156line 153 didn't jump to line 156, because the condition on line 153 was never true
154 # We set a default delegate for composites so that a class is
155 # guaranteed to support something if it is a composite.
156 log.debug("Setting default delegate for %s", self.name)
157 self._delegate = self.defaultDelegate
158 self._delegateClassName = self.defaultDelegateName
159 else:
160 self._delegate = None
161 self._delegateClassName = None
163 @property
164 def components(self) -> Dict[str, StorageClass]:
165 """Return the components associated with this `StorageClass`."""
166 return self._components
168 @property
169 def derivedComponents(self) -> Dict[str, StorageClass]:
170 """Return derived components associated with `StorageClass`."""
171 return self._derivedComponents
173 @property
174 def converters(self) -> Dict[str, str]:
175 """Return the type converters supported by this `StorageClass`."""
176 return self._converters
178 @property
179 def converters_by_type(self) -> Dict[Type, Type]:
180 """Return the type converters as python types."""
181 if self._converters_by_type is None:
182 self._converters_by_type = {}
184 # Loop over list because the dict can be edited in loop.
185 for candidate_type_str, converter_str in list(self.converters.items()):
186 if hasattr(builtins, candidate_type_str):
187 candidate_type = getattr(builtins, candidate_type_str)
188 else:
189 try:
190 candidate_type = doImportType(candidate_type_str)
191 except ImportError as e:
192 log.warning(
193 "Unable to import type %s associated with storage class %s (%s)",
194 candidate_type_str,
195 self.name,
196 e,
197 )
198 del self.converters[candidate_type_str]
199 continue
201 try:
202 converter = doImportType(converter_str)
203 except ImportError as e:
204 log.warning(
205 "Unable to import conversion function %s associated with storage class %s "
206 "required to convert type %s (%s)",
207 converter_str,
208 self.name,
209 candidate_type_str,
210 e,
211 )
212 del self.converters[candidate_type_str]
213 continue
214 if not callable(converter):
215 # doImportType is annotated to return a Type but in actual
216 # fact it can return Any except ModuleType because package
217 # variables can be accessed. This make mypy believe it
218 # is impossible for the return value to not be a callable
219 # so we must ignore the warning.
220 log.warning( # type: ignore
221 "Conversion function %s associated with storage class "
222 "%s to convert type %s is not a callable.",
223 converter_str,
224 self.name,
225 candidate_type_str,
226 )
227 del self.converters[candidate_type_str]
228 continue
229 self._converters_by_type[candidate_type] = converter
230 return self._converters_by_type
232 @property
233 def parameters(self) -> Set[str]:
234 """Return `set` of names of supported parameters."""
235 return set(self._parameters)
237 @property
238 def pytype(self) -> Type:
239 """Return Python type associated with this `StorageClass`."""
240 if self._pytype is not None:
241 return self._pytype
243 if hasattr(builtins, self._pytypeName):
244 pytype = getattr(builtins, self._pytypeName)
245 else:
246 pytype = doImportType(self._pytypeName)
247 self._pytype = pytype
248 return self._pytype
250 @property
251 def delegateClass(self) -> Optional[Type]:
252 """Class to use to delegate type-specific actions."""
253 if self._delegate is not None:
254 return self._delegate
255 if self._delegateClassName is None:
256 return None
257 delegate_class = doImportType(self._delegateClassName)
258 self._delegate = delegate_class
259 return self._delegate
261 def allComponents(self) -> Mapping[str, StorageClass]:
262 """Return all defined components.
264 This mapping includes all the derived and read/write components
265 for the corresponding storage class.
267 Returns
268 -------
269 comp : `dict` of [`str`, `StorageClass`]
270 The component name to storage class mapping.
271 """
272 components = copy.copy(self.components)
273 components.update(self.derivedComponents)
274 return components
276 def delegate(self) -> StorageClassDelegate:
277 """Return an instance of a storage class delegate.
279 Returns
280 -------
281 delegate : `StorageClassDelegate`
282 Instance of the delegate associated with this `StorageClass`.
283 The delegate is constructed with this `StorageClass`.
285 Raises
286 ------
287 TypeError
288 This StorageClass has no associated delegate.
289 """
290 cls = self.delegateClass
291 if cls is None:
292 raise TypeError(f"No delegate class is associated with StorageClass {self.name}")
293 return cls(storageClass=self)
295 def isComposite(self) -> bool:
296 """Return Boolean indicating whether this is a composite or not.
298 Returns
299 -------
300 isComposite : `bool`
301 `True` if this `StorageClass` is a composite, `False`
302 otherwise.
303 """
304 if self.components:
305 return True
306 return False
308 def _lookupNames(self) -> Tuple[LookupKey, ...]:
309 """Keys to use when looking up this DatasetRef in a configuration.
311 The names are returned in order of priority.
313 Returns
314 -------
315 names : `tuple` of `LookupKey`
316 Tuple of a `LookupKey` using the `StorageClass` name.
317 """
318 return (LookupKey(name=self.name),)
320 def knownParameters(self) -> Set[str]:
321 """Return set of all parameters known to this `StorageClass`.
323 The set includes parameters understood by components of a composite.
325 Returns
326 -------
327 known : `set`
328 All parameter keys of this `StorageClass` and the component
329 storage classes.
330 """
331 known = set(self._parameters)
332 for sc in self.components.values():
333 known.update(sc.knownParameters())
334 return known
336 def validateParameters(self, parameters: Collection = None) -> None:
337 """Check that the parameters are known to this `StorageClass`.
339 Does not check the values.
341 Parameters
342 ----------
343 parameters : `~collections.abc.Collection`, optional
344 Collection containing the parameters. Can be `dict`-like or
345 `set`-like. The parameter values are not checked.
346 If no parameters are supplied, always returns without error.
348 Raises
349 ------
350 KeyError
351 Some parameters are not understood by this `StorageClass`.
352 """
353 # No parameters is always okay
354 if not parameters:
355 return
357 # Extract the important information into a set. Works for dict and
358 # list.
359 external = set(parameters)
361 diff = external - self.knownParameters()
362 if diff:
363 s = "s" if len(diff) > 1 else ""
364 unknown = "', '".join(diff)
365 raise KeyError(f"Parameter{s} '{unknown}' not understood by StorageClass {self.name}")
367 def filterParameters(self, parameters: Dict[str, Any], subset: Collection = None) -> Dict[str, Any]:
368 """Filter out parameters that are not known to this `StorageClass`.
370 Parameters
371 ----------
372 parameters : `dict`, optional
373 Candidate parameters. Can be `None` if no parameters have
374 been provided.
375 subset : `~collections.abc.Collection`, optional
376 Subset of supported parameters that the caller is interested
377 in using. The subset must be known to the `StorageClass`
378 if specified. If `None` the supplied parameters will all
379 be checked, else only the keys in this set will be checked.
381 Returns
382 -------
383 filtered : `dict`
384 Valid parameters. Empty `dict` if none are suitable.
386 Raises
387 ------
388 ValueError
389 Raised if the provided subset is not a subset of the supported
390 parameters or if it is an empty set.
391 """
392 if not parameters:
393 return {}
395 known = self.knownParameters()
397 if subset is not None:
398 if not subset:
399 raise ValueError("Specified a parameter subset but it was empty")
400 subset = set(subset)
401 if not subset.issubset(known):
402 raise ValueError(f"Requested subset ({subset}) is not a subset of known parameters ({known})")
403 wanted = subset
404 else:
405 wanted = known
407 return {k: parameters[k] for k in wanted if k in parameters}
409 def validateInstance(self, instance: Any) -> bool:
410 """Check that the supplied Python object has the expected Python type.
412 Parameters
413 ----------
414 instance : `object`
415 Object to check.
417 Returns
418 -------
419 isOk : `bool`
420 True if the supplied instance object can be handled by this
421 `StorageClass`, False otherwise.
422 """
423 return isinstance(instance, self.pytype)
425 def can_convert(self, other: StorageClass) -> bool:
426 """Return `True` if this storage class can convert python types
427 in the other storage class.
429 Parameters
430 ----------
431 other : `StorageClass`
432 The storage class to check.
434 Returns
435 -------
436 can : `bool`
437 `True` if the two storage classes are compatible.
438 """
439 if other.name == self.name:
440 # Identical storage classes are compatible.
441 return True
443 for candidate_type in self.converters_by_type:
444 if issubclass(other.pytype, candidate_type):
445 return True
446 return False
448 def coerce_type(self, incorrect: Any) -> Any:
449 """Coerce the supplied incorrect instance to the python type
450 associated with this `StorageClass`.
452 Parameters
453 ----------
454 incorrect : `object`
455 An object that might be the incorrect type.
457 Returns
458 -------
459 correct : `object`
460 An object that matches the python type of this `StorageClass`.
461 Can be the same object as given. If `None`, `None` will be
462 returned.
464 Raises
465 ------
466 TypeError
467 Raised if no conversion can be found.
468 """
469 if incorrect is None:
470 return None
472 # Possible this is the correct type already.
473 if self.validateInstance(incorrect):
474 return incorrect
476 # Check each registered converter.
477 for candidate_type, converter in self.converters_by_type.items():
478 if isinstance(incorrect, candidate_type):
479 try:
480 return converter(incorrect)
481 except Exception:
482 log.error(
483 "Converter %s failed to convert type %s",
484 get_full_type_name(converter),
485 get_full_type_name(incorrect),
486 )
487 raise
488 raise TypeError(
489 "Type does not match and no valid converter found to convert"
490 f" '{type(incorrect)}' to '{self.pytype}'"
491 )
493 def __eq__(self, other: Any) -> bool:
494 """Equality checks name, pytype name, delegate name, and components."""
495 if not isinstance(other, StorageClass):
496 return NotImplemented
498 if self.name != other.name:
499 return False
501 # We must compare pytype and delegate by name since we do not want
502 # to trigger an import of external module code here
503 if self._delegateClassName != other._delegateClassName:
504 return False
505 if self._pytypeName != other._pytypeName:
506 return False
508 # Ensure we have the same component keys in each
509 if set(self.components.keys()) != set(other.components.keys()):
510 return False
512 # Same parameters
513 if self.parameters != other.parameters:
514 return False
516 # Ensure that all the components have the same type
517 for k in self.components:
518 if self.components[k] != other.components[k]:
519 return False
521 # If we got to this point everything checks out
522 return True
524 def __hash__(self) -> int:
525 return hash(self.name)
527 def __repr__(self) -> str:
528 optionals: Dict[str, Any] = {}
529 if self._pytypeName != "object":
530 optionals["pytype"] = self._pytypeName
531 if self._delegateClassName is not None:
532 optionals["delegate"] = self._delegateClassName
533 if self._parameters:
534 optionals["parameters"] = self._parameters
535 if self.components:
536 optionals["components"] = self.components
537 if self.converters:
538 optionals["converters"] = self.converters
540 # order is preserved in the dict
541 options = ", ".join(f"{k}={v!r}" for k, v in optionals.items())
543 # Start with mandatory fields
544 r = f"{self.__class__.__name__}({self.name!r}"
545 if options:
546 r = r + ", " + options
547 r = r + ")"
548 return r
550 def __str__(self) -> str:
551 return self.name
554class StorageClassFactory(metaclass=Singleton):
555 """Factory for `StorageClass` instances.
557 This class is a singleton, with each instance sharing the pool of
558 StorageClasses. Since code can not know whether it is the first
559 time the instance has been created, the constructor takes no arguments.
560 To populate the factory with storage classes, a call to
561 `~StorageClassFactory.addFromConfig()` should be made.
563 Parameters
564 ----------
565 config : `StorageClassConfig` or `str`, optional
566 Load configuration. In a ButlerConfig` the relevant configuration
567 is located in the ``storageClasses`` section.
568 """
570 def __init__(self, config: Optional[Union[StorageClassConfig, str]] = None):
571 self._storageClasses: Dict[str, StorageClass] = {}
572 self._configs: List[StorageClassConfig] = []
574 # Always seed with the default config
575 self.addFromConfig(StorageClassConfig())
577 if config is not None: 577 ↛ 578line 577 didn't jump to line 578, because the condition on line 577 was never true
578 self.addFromConfig(config)
580 def __str__(self) -> str:
581 """Return summary of factory.
583 Returns
584 -------
585 summary : `str`
586 Summary of the factory status.
587 """
588 sep = "\n"
589 return f"""Number of registered StorageClasses: {len(self._storageClasses)}
591StorageClasses
592--------------
593{sep.join(f"{s}: {self._storageClasses[s]}" for s in self._storageClasses)}
594"""
596 def __contains__(self, storageClassOrName: Union[StorageClass, str]) -> bool:
597 """Indicate whether the storage class exists in the factory.
599 Parameters
600 ----------
601 storageClassOrName : `str` or `StorageClass`
602 If `str` is given existence of the named StorageClass
603 in the factory is checked. If `StorageClass` is given
604 existence and equality are checked.
606 Returns
607 -------
608 in : `bool`
609 True if the supplied string is present, or if the supplied
610 `StorageClass` is present and identical.
612 Notes
613 -----
614 The two different checks (one for "key" and one for "value") based on
615 the type of the given argument mean that it is possible for
616 StorageClass.name to be in the factory but StorageClass to not be
617 in the factory.
618 """
619 if isinstance(storageClassOrName, str): 619 ↛ 621line 619 didn't jump to line 621, because the condition on line 619 was never false
620 return storageClassOrName in self._storageClasses
621 elif isinstance(storageClassOrName, StorageClass):
622 if storageClassOrName.name in self._storageClasses:
623 return storageClassOrName == self._storageClasses[storageClassOrName.name]
624 return False
626 def addFromConfig(self, config: Union[StorageClassConfig, Config, str]) -> None:
627 """Add more `StorageClass` definitions from a config file.
629 Parameters
630 ----------
631 config : `StorageClassConfig`, `Config` or `str`
632 Storage class configuration. Can contain a ``storageClasses``
633 key if part of a global configuration.
634 """
635 sconfig = StorageClassConfig(config)
636 self._configs.append(sconfig)
638 # Since we can not assume that we will get definitions of
639 # components or parents before their classes are defined
640 # we have a helper function that we can call recursively
641 # to extract definitions from the configuration.
642 def processStorageClass(name: str, sconfig: StorageClassConfig) -> None:
643 # Maybe we've already processed this through recursion
644 if name not in sconfig:
645 return
646 info = sconfig.pop(name)
648 # Always create the storage class so we can ensure that
649 # we are not trying to overwrite with a different definition
650 components = None
652 # Extract scalar items from dict that are needed for
653 # StorageClass Constructor
654 storageClassKwargs = {k: info[k] for k in ("pytype", "delegate", "parameters") if k in info}
656 if "converters" in info:
657 storageClassKwargs["converters"] = info["converters"].toDict()
659 for compName in ("components", "derivedComponents"):
660 if compName not in info:
661 continue
662 components = {}
663 for cname, ctype in info[compName].items():
664 if ctype not in self:
665 processStorageClass(ctype, sconfig)
666 components[cname] = self.getStorageClass(ctype)
668 # Fill in other items
669 storageClassKwargs[compName] = components
671 # Create the new storage class and register it
672 baseClass = None
673 if "inheritsFrom" in info:
674 baseName = info["inheritsFrom"]
675 if baseName not in self: 675 ↛ 676line 675 didn't jump to line 676, because the condition on line 675 was never true
676 processStorageClass(baseName, sconfig)
677 baseClass = type(self.getStorageClass(baseName))
679 newStorageClassType = self.makeNewStorageClass(name, baseClass, **storageClassKwargs)
680 newStorageClass = newStorageClassType()
681 self.registerStorageClass(newStorageClass)
683 for name in list(sconfig.keys()):
684 processStorageClass(name, sconfig)
686 @staticmethod
687 def makeNewStorageClass(
688 name: str, baseClass: Optional[Type[StorageClass]] = StorageClass, **kwargs: Any
689 ) -> Type[StorageClass]:
690 """Create a new Python class as a subclass of `StorageClass`.
692 Parameters
693 ----------
694 name : `str`
695 Name to use for this class.
696 baseClass : `type`, optional
697 Base class for this `StorageClass`. Must be either `StorageClass`
698 or a subclass of `StorageClass`. If `None`, `StorageClass` will
699 be used.
701 Returns
702 -------
703 newtype : `type` subclass of `StorageClass`
704 Newly created Python type.
705 """
706 if baseClass is None:
707 baseClass = StorageClass
708 if not issubclass(baseClass, StorageClass): 708 ↛ 709line 708 didn't jump to line 709, because the condition on line 708 was never true
709 raise ValueError(f"Base class must be a StorageClass not {baseClass}")
711 # convert the arguments to use different internal names
712 clsargs = {f"_cls_{k}": v for k, v in kwargs.items() if v is not None}
713 clsargs["_cls_name"] = name
715 # Some container items need to merge with the base class values
716 # so that a child can inherit but override one bit.
717 # lists (which you get from configs) are treated as sets for this to
718 # work consistently.
719 for k in ("components", "parameters", "derivedComponents", "converters"):
720 classKey = f"_cls_{k}"
721 if classKey in clsargs:
722 baseValue = getattr(baseClass, classKey, None)
723 if baseValue is not None:
724 currentValue = clsargs[classKey]
725 if isinstance(currentValue, dict): 725 ↛ 728line 725 didn't jump to line 728, because the condition on line 725 was never false
726 newValue = baseValue.copy()
727 else:
728 newValue = set(baseValue)
729 newValue.update(currentValue)
730 clsargs[classKey] = newValue
732 # If we have parameters they should be a frozen set so that the
733 # parameters in the class can not be modified.
734 pk = "_cls_parameters"
735 if pk in clsargs:
736 clsargs[pk] = frozenset(clsargs[pk])
738 return type(f"StorageClass{name}", (baseClass,), clsargs)
740 def getStorageClass(self, storageClassName: str) -> StorageClass:
741 """Get a StorageClass instance associated with the supplied name.
743 Parameters
744 ----------
745 storageClassName : `str`
746 Name of the storage class to retrieve.
748 Returns
749 -------
750 instance : `StorageClass`
751 Instance of the correct `StorageClass`.
753 Raises
754 ------
755 KeyError
756 The requested storage class name is not registered.
757 """
758 return self._storageClasses[storageClassName]
760 def registerStorageClass(self, storageClass: StorageClass) -> None:
761 """Store the `StorageClass` in the factory.
763 Will be indexed by `StorageClass.name` and will return instances
764 of the supplied `StorageClass`.
766 Parameters
767 ----------
768 storageClass : `StorageClass`
769 Type of the Python `StorageClass` to register.
771 Raises
772 ------
773 ValueError
774 If a storage class has already been registered with
775 storageClassName and the previous definition differs.
776 """
777 if storageClass.name in self._storageClasses: 777 ↛ 778line 777 didn't jump to line 778, because the condition on line 777 was never true
778 existing = self.getStorageClass(storageClass.name)
779 if existing != storageClass:
780 raise ValueError(
781 f"New definition for StorageClass {storageClass.name} ({storageClass}) "
782 f"differs from current definition ({existing})"
783 )
784 else:
785 self._storageClasses[storageClass.name] = storageClass
787 def _unregisterStorageClass(self, storageClassName: str) -> None:
788 """Remove the named StorageClass from the factory.
790 Parameters
791 ----------
792 storageClassName : `str`
793 Name of storage class to remove.
795 Raises
796 ------
797 KeyError
798 The named storage class is not registered.
800 Notes
801 -----
802 This method is intended to simplify testing of StorageClassFactory
803 functionality and it is not expected to be required for normal usage.
804 """
805 del self._storageClasses[storageClassName]