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

311 statements  

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/>. 

21 

22from __future__ import annotations 

23 

24"""Support for Storage Classes.""" 

25 

26__all__ = ("StorageClass", "StorageClassFactory", "StorageClassConfig") 

27 

28import builtins 

29import copy 

30import logging 

31from typing import Any, Collection, Dict, List, Mapping, Optional, Sequence, Set, Tuple, Type, Union 

32 

33from lsst.utils import doImportType 

34from lsst.utils.classes import Singleton 

35from lsst.utils.introspection import get_full_type_name 

36 

37from .config import Config, ConfigSubset 

38from .configSupport import LookupKey 

39from .storageClassDelegate import StorageClassDelegate 

40 

41log = logging.getLogger(__name__) 

42 

43 

44class StorageClassConfig(ConfigSubset): 

45 """Configuration class for defining Storage Classes.""" 

46 

47 component = "storageClasses" 

48 defaultConfigFile = "storageClasses.yaml" 

49 

50 

51class StorageClass: 

52 """Class describing how a label maps to a particular Python type. 

53 

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 """ 

74 

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) 

84 

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 

107 

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) 

114 

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 

118 

119 self.name = name 

120 

121 if pytype is None: 

122 pytype = object 

123 

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 

133 

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 

162 

163 @property 

164 def components(self) -> Dict[str, StorageClass]: 

165 """Return the components associated with this `StorageClass`.""" 

166 return self._components 

167 

168 @property 

169 def derivedComponents(self) -> Dict[str, StorageClass]: 

170 """Return derived components associated with `StorageClass`.""" 

171 return self._derivedComponents 

172 

173 @property 

174 def converters(self) -> Dict[str, str]: 

175 """Return the type converters supported by this `StorageClass`.""" 

176 return self._converters 

177 

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 = {} 

183 

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 

200 

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 

231 

232 @property 

233 def parameters(self) -> Set[str]: 

234 """Return `set` of names of supported parameters.""" 

235 return set(self._parameters) 

236 

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 

242 

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 

249 

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 

260 

261 def allComponents(self) -> Mapping[str, StorageClass]: 

262 """Return all defined components. 

263 

264 This mapping includes all the derived and read/write components 

265 for the corresponding storage class. 

266 

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 

275 

276 def delegate(self) -> StorageClassDelegate: 

277 """Return an instance of a storage class delegate. 

278 

279 Returns 

280 ------- 

281 delegate : `StorageClassDelegate` 

282 Instance of the delegate associated with this `StorageClass`. 

283 The delegate is constructed with this `StorageClass`. 

284 

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) 

294 

295 def isComposite(self) -> bool: 

296 """Return Boolean indicating whether this is a composite or not. 

297 

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 

307 

308 def _lookupNames(self) -> Tuple[LookupKey, ...]: 

309 """Keys to use when looking up this DatasetRef in a configuration. 

310 

311 The names are returned in order of priority. 

312 

313 Returns 

314 ------- 

315 names : `tuple` of `LookupKey` 

316 Tuple of a `LookupKey` using the `StorageClass` name. 

317 """ 

318 return (LookupKey(name=self.name),) 

319 

320 def knownParameters(self) -> Set[str]: 

321 """Return set of all parameters known to this `StorageClass`. 

322 

323 The set includes parameters understood by components of a composite. 

324 

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 

335 

336 def validateParameters(self, parameters: Collection = None) -> None: 

337 """Check that the parameters are known to this `StorageClass`. 

338 

339 Does not check the values. 

340 

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. 

347 

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 

356 

357 # Extract the important information into a set. Works for dict and 

358 # list. 

359 external = set(parameters) 

360 

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}") 

366 

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`. 

369 

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. 

380 

381 Returns 

382 ------- 

383 filtered : `dict` 

384 Valid parameters. Empty `dict` if none are suitable. 

385 

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 {} 

394 

395 known = self.knownParameters() 

396 

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 

406 

407 return {k: parameters[k] for k in wanted if k in parameters} 

408 

409 def validateInstance(self, instance: Any) -> bool: 

410 """Check that the supplied Python object has the expected Python type. 

411 

412 Parameters 

413 ---------- 

414 instance : `object` 

415 Object to check. 

416 

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) 

424 

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. 

428 

429 Parameters 

430 ---------- 

431 other : `StorageClass` 

432 The storage class to check. 

433 

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 

442 

443 for candidate_type in self.converters_by_type: 

444 if issubclass(other.pytype, candidate_type): 

445 return True 

446 return False 

447 

448 def coerce_type(self, incorrect: Any) -> Any: 

449 """Coerce the supplied incorrect instance to the python type 

450 associated with this `StorageClass`. 

451 

452 Parameters 

453 ---------- 

454 incorrect : `object` 

455 An object that might be the incorrect type. 

456 

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. 

463 

464 Raises 

465 ------ 

466 TypeError 

467 Raised if no conversion can be found. 

468 """ 

469 if incorrect is None: 

470 return None 

471 

472 # Possible this is the correct type already. 

473 if self.validateInstance(incorrect): 

474 return incorrect 

475 

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 ) 

492 

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 

497 

498 if self.name != other.name: 

499 return False 

500 

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 

507 

508 # Ensure we have the same component keys in each 

509 if set(self.components.keys()) != set(other.components.keys()): 

510 return False 

511 

512 # Same parameters 

513 if self.parameters != other.parameters: 

514 return False 

515 

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 

520 

521 # If we got to this point everything checks out 

522 return True 

523 

524 def __hash__(self) -> int: 

525 return hash(self.name) 

526 

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 

539 

540 # order is preserved in the dict 

541 options = ", ".join(f"{k}={v!r}" for k, v in optionals.items()) 

542 

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 

549 

550 def __str__(self) -> str: 

551 return self.name 

552 

553 

554class StorageClassFactory(metaclass=Singleton): 

555 """Factory for `StorageClass` instances. 

556 

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. 

562 

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 """ 

569 

570 def __init__(self, config: Optional[Union[StorageClassConfig, str]] = None): 

571 self._storageClasses: Dict[str, StorageClass] = {} 

572 self._configs: List[StorageClassConfig] = [] 

573 

574 # Always seed with the default config 

575 self.addFromConfig(StorageClassConfig()) 

576 

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) 

579 

580 def __str__(self) -> str: 

581 """Return summary of factory. 

582 

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)} 

590 

591StorageClasses 

592-------------- 

593{sep.join(f"{s}: {self._storageClasses[s]}" for s in self._storageClasses)} 

594""" 

595 

596 def __contains__(self, storageClassOrName: Union[StorageClass, str]) -> bool: 

597 """Indicate whether the storage class exists in the factory. 

598 

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. 

605 

606 Returns 

607 ------- 

608 in : `bool` 

609 True if the supplied string is present, or if the supplied 

610 `StorageClass` is present and identical. 

611 

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 

625 

626 def addFromConfig(self, config: Union[StorageClassConfig, Config, str]) -> None: 

627 """Add more `StorageClass` definitions from a config file. 

628 

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) 

637 

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) 

647 

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 

651 

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} 

655 

656 if "converters" in info: 

657 storageClassKwargs["converters"] = info["converters"].toDict() 

658 

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) 

667 

668 # Fill in other items 

669 storageClassKwargs[compName] = components 

670 

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)) 

678 

679 newStorageClassType = self.makeNewStorageClass(name, baseClass, **storageClassKwargs) 

680 newStorageClass = newStorageClassType() 

681 self.registerStorageClass(newStorageClass) 

682 

683 for name in list(sconfig.keys()): 

684 processStorageClass(name, sconfig) 

685 

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`. 

691 

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. 

700 

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}") 

710 

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 

714 

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 

731 

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]) 

737 

738 return type(f"StorageClass{name}", (baseClass,), clsargs) 

739 

740 def getStorageClass(self, storageClassName: str) -> StorageClass: 

741 """Get a StorageClass instance associated with the supplied name. 

742 

743 Parameters 

744 ---------- 

745 storageClassName : `str` 

746 Name of the storage class to retrieve. 

747 

748 Returns 

749 ------- 

750 instance : `StorageClass` 

751 Instance of the correct `StorageClass`. 

752 

753 Raises 

754 ------ 

755 KeyError 

756 The requested storage class name is not registered. 

757 """ 

758 return self._storageClasses[storageClassName] 

759 

760 def registerStorageClass(self, storageClass: StorageClass) -> None: 

761 """Store the `StorageClass` in the factory. 

762 

763 Will be indexed by `StorageClass.name` and will return instances 

764 of the supplied `StorageClass`. 

765 

766 Parameters 

767 ---------- 

768 storageClass : `StorageClass` 

769 Type of the Python `StorageClass` to register. 

770 

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 

786 

787 def _unregisterStorageClass(self, storageClassName: str) -> None: 

788 """Remove the named StorageClass from the factory. 

789 

790 Parameters 

791 ---------- 

792 storageClassName : `str` 

793 Name of storage class to remove. 

794 

795 Raises 

796 ------ 

797 KeyError 

798 The named storage class is not registered. 

799 

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]