Coverage for python/lsst/daf/butler/core/storageClass.py: 46%

322 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-17 02:08 -0700

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: Mapping[str, Any], subset: Collection = None) -> Mapping[str, Any]: 

368 """Filter out parameters that are not known to this `StorageClass`. 

369 

370 Parameters 

371 ---------- 

372 parameters : `Mapping`, 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 : `Mapping` 

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 this storage class has a registered converter for 

438 the python type associated with the other storage class. That 

439 converter will convert the other python type to the one associated 

440 with this storage class. 

441 """ 

442 if other.name == self.name: 

443 # Identical storage classes are compatible. 

444 return True 

445 

446 # It may be that the storage class being compared is not 

447 # available because the python type can't be imported. In that 

448 # case conversion must be impossible. 

449 try: 

450 other_pytype = other.pytype 

451 except Exception: 

452 return False 

453 

454 # Or even this storage class itself can not have the type imported. 

455 try: 

456 self_pytype = self.pytype 

457 except Exception: 

458 return False 

459 

460 if issubclass(other_pytype, self_pytype): 

461 # Storage classes have different names but the same python type. 

462 return True 

463 

464 for candidate_type in self.converters_by_type: 

465 if issubclass(other_pytype, candidate_type): 

466 return True 

467 return False 

468 

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

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

471 associated with this `StorageClass`. 

472 

473 Parameters 

474 ---------- 

475 incorrect : `object` 

476 An object that might be the incorrect type. 

477 

478 Returns 

479 ------- 

480 correct : `object` 

481 An object that matches the python type of this `StorageClass`. 

482 Can be the same object as given. If `None`, `None` will be 

483 returned. 

484 

485 Raises 

486 ------ 

487 TypeError 

488 Raised if no conversion can be found. 

489 """ 

490 if incorrect is None: 

491 return None 

492 

493 # Possible this is the correct type already. 

494 if self.validateInstance(incorrect): 

495 return incorrect 

496 

497 # Check each registered converter. 

498 for candidate_type, converter in self.converters_by_type.items(): 

499 if isinstance(incorrect, candidate_type): 

500 try: 

501 return converter(incorrect) 

502 except Exception: 

503 log.error( 

504 "Converter %s failed to convert type %s", 

505 get_full_type_name(converter), 

506 get_full_type_name(incorrect), 

507 ) 

508 raise 

509 raise TypeError( 

510 "Type does not match and no valid converter found to convert" 

511 f" '{type(incorrect)}' to '{self.pytype}'" 

512 ) 

513 

514 def __eq__(self, other: Any) -> bool: 

515 """Equality checks name, pytype name, delegate name, and components.""" 

516 if not isinstance(other, StorageClass): 

517 return NotImplemented 

518 

519 if self.name != other.name: 

520 return False 

521 

522 # We must compare pytype and delegate by name since we do not want 

523 # to trigger an import of external module code here 

524 if self._delegateClassName != other._delegateClassName: 

525 return False 

526 if self._pytypeName != other._pytypeName: 

527 return False 

528 

529 # Ensure we have the same component keys in each 

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

531 return False 

532 

533 # Same parameters 

534 if self.parameters != other.parameters: 

535 return False 

536 

537 # Ensure that all the components have the same type 

538 for k in self.components: 

539 if self.components[k] != other.components[k]: 

540 return False 

541 

542 # If we got to this point everything checks out 

543 return True 

544 

545 def __hash__(self) -> int: 

546 return hash(self.name) 

547 

548 def __repr__(self) -> str: 

549 optionals: Dict[str, Any] = {} 

550 if self._pytypeName != "object": 

551 optionals["pytype"] = self._pytypeName 

552 if self._delegateClassName is not None: 

553 optionals["delegate"] = self._delegateClassName 

554 if self._parameters: 

555 optionals["parameters"] = self._parameters 

556 if self.components: 

557 optionals["components"] = self.components 

558 if self.converters: 

559 optionals["converters"] = self.converters 

560 

561 # order is preserved in the dict 

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

563 

564 # Start with mandatory fields 

565 r = f"{self.__class__.__name__}({self.name!r}" 

566 if options: 

567 r = r + ", " + options 

568 r = r + ")" 

569 return r 

570 

571 def __str__(self) -> str: 

572 return self.name 

573 

574 

575class StorageClassFactory(metaclass=Singleton): 

576 """Factory for `StorageClass` instances. 

577 

578 This class is a singleton, with each instance sharing the pool of 

579 StorageClasses. Since code can not know whether it is the first 

580 time the instance has been created, the constructor takes no arguments. 

581 To populate the factory with storage classes, a call to 

582 `~StorageClassFactory.addFromConfig()` should be made. 

583 

584 Parameters 

585 ---------- 

586 config : `StorageClassConfig` or `str`, optional 

587 Load configuration. In a ButlerConfig` the relevant configuration 

588 is located in the ``storageClasses`` section. 

589 """ 

590 

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

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

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

594 

595 # Always seed with the default config 

596 self.addFromConfig(StorageClassConfig()) 

597 

598 if config is not None: 598 ↛ 599line 598 didn't jump to line 599, because the condition on line 598 was never true

599 self.addFromConfig(config) 

600 

601 def __str__(self) -> str: 

602 """Return summary of factory. 

603 

604 Returns 

605 ------- 

606 summary : `str` 

607 Summary of the factory status. 

608 """ 

609 sep = "\n" 

610 return f"""Number of registered StorageClasses: {len(self._storageClasses)} 

611 

612StorageClasses 

613-------------- 

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

615""" 

616 

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

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

619 

620 Parameters 

621 ---------- 

622 storageClassOrName : `str` or `StorageClass` 

623 If `str` is given existence of the named StorageClass 

624 in the factory is checked. If `StorageClass` is given 

625 existence and equality are checked. 

626 

627 Returns 

628 ------- 

629 in : `bool` 

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

631 `StorageClass` is present and identical. 

632 

633 Notes 

634 ----- 

635 The two different checks (one for "key" and one for "value") based on 

636 the type of the given argument mean that it is possible for 

637 StorageClass.name to be in the factory but StorageClass to not be 

638 in the factory. 

639 """ 

640 if isinstance(storageClassOrName, str): 640 ↛ 642line 640 didn't jump to line 642, because the condition on line 640 was never false

641 return storageClassOrName in self._storageClasses 

642 elif isinstance(storageClassOrName, StorageClass): 

643 if storageClassOrName.name in self._storageClasses: 

644 return storageClassOrName == self._storageClasses[storageClassOrName.name] 

645 return False 

646 

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

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

649 

650 Parameters 

651 ---------- 

652 config : `StorageClassConfig`, `Config` or `str` 

653 Storage class configuration. Can contain a ``storageClasses`` 

654 key if part of a global configuration. 

655 """ 

656 sconfig = StorageClassConfig(config) 

657 self._configs.append(sconfig) 

658 

659 # Since we can not assume that we will get definitions of 

660 # components or parents before their classes are defined 

661 # we have a helper function that we can call recursively 

662 # to extract definitions from the configuration. 

663 def processStorageClass(name: str, sconfig: StorageClassConfig) -> None: 

664 # Maybe we've already processed this through recursion 

665 if name not in sconfig: 

666 return 

667 info = sconfig.pop(name) 

668 

669 # Always create the storage class so we can ensure that 

670 # we are not trying to overwrite with a different definition 

671 components = None 

672 

673 # Extract scalar items from dict that are needed for 

674 # StorageClass Constructor 

675 storageClassKwargs = {k: info[k] for k in ("pytype", "delegate", "parameters") if k in info} 

676 

677 if "converters" in info: 

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

679 

680 for compName in ("components", "derivedComponents"): 

681 if compName not in info: 

682 continue 

683 components = {} 

684 for cname, ctype in info[compName].items(): 

685 if ctype not in self: 

686 processStorageClass(ctype, sconfig) 

687 components[cname] = self.getStorageClass(ctype) 

688 

689 # Fill in other items 

690 storageClassKwargs[compName] = components 

691 

692 # Create the new storage class and register it 

693 baseClass = None 

694 if "inheritsFrom" in info: 

695 baseName = info["inheritsFrom"] 

696 if baseName not in self: 696 ↛ 697line 696 didn't jump to line 697, because the condition on line 696 was never true

697 processStorageClass(baseName, sconfig) 

698 baseClass = type(self.getStorageClass(baseName)) 

699 

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

701 newStorageClass = newStorageClassType() 

702 self.registerStorageClass(newStorageClass) 

703 

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

705 processStorageClass(name, sconfig) 

706 

707 @staticmethod 

708 def makeNewStorageClass( 

709 name: str, baseClass: Optional[Type[StorageClass]] = StorageClass, **kwargs: Any 

710 ) -> Type[StorageClass]: 

711 """Create a new Python class as a subclass of `StorageClass`. 

712 

713 Parameters 

714 ---------- 

715 name : `str` 

716 Name to use for this class. 

717 baseClass : `type`, optional 

718 Base class for this `StorageClass`. Must be either `StorageClass` 

719 or a subclass of `StorageClass`. If `None`, `StorageClass` will 

720 be used. 

721 

722 Returns 

723 ------- 

724 newtype : `type` subclass of `StorageClass` 

725 Newly created Python type. 

726 """ 

727 if baseClass is None: 

728 baseClass = StorageClass 

729 if not issubclass(baseClass, StorageClass): 729 ↛ 730line 729 didn't jump to line 730, because the condition on line 729 was never true

730 raise ValueError(f"Base class must be a StorageClass not {baseClass}") 

731 

732 # convert the arguments to use different internal names 

733 clsargs = {f"_cls_{k}": v for k, v in kwargs.items() if v is not None} 

734 clsargs["_cls_name"] = name 

735 

736 # Some container items need to merge with the base class values 

737 # so that a child can inherit but override one bit. 

738 # lists (which you get from configs) are treated as sets for this to 

739 # work consistently. 

740 for k in ("components", "parameters", "derivedComponents", "converters"): 

741 classKey = f"_cls_{k}" 

742 if classKey in clsargs: 

743 baseValue = getattr(baseClass, classKey, None) 

744 if baseValue is not None: 

745 currentValue = clsargs[classKey] 

746 if isinstance(currentValue, dict): 746 ↛ 749line 746 didn't jump to line 749, because the condition on line 746 was never false

747 newValue = baseValue.copy() 

748 else: 

749 newValue = set(baseValue) 

750 newValue.update(currentValue) 

751 clsargs[classKey] = newValue 

752 

753 # If we have parameters they should be a frozen set so that the 

754 # parameters in the class can not be modified. 

755 pk = "_cls_parameters" 

756 if pk in clsargs: 

757 clsargs[pk] = frozenset(clsargs[pk]) 

758 

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

760 

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

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

763 

764 Parameters 

765 ---------- 

766 storageClassName : `str` 

767 Name of the storage class to retrieve. 

768 

769 Returns 

770 ------- 

771 instance : `StorageClass` 

772 Instance of the correct `StorageClass`. 

773 

774 Raises 

775 ------ 

776 KeyError 

777 The requested storage class name is not registered. 

778 """ 

779 return self._storageClasses[storageClassName] 

780 

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

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

783 

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

785 of the supplied `StorageClass`. 

786 

787 Parameters 

788 ---------- 

789 storageClass : `StorageClass` 

790 Type of the Python `StorageClass` to register. 

791 

792 Raises 

793 ------ 

794 ValueError 

795 If a storage class has already been registered with 

796 storageClassName and the previous definition differs. 

797 """ 

798 if storageClass.name in self._storageClasses: 798 ↛ 799line 798 didn't jump to line 799, because the condition on line 798 was never true

799 existing = self.getStorageClass(storageClass.name) 

800 if existing != storageClass: 

801 raise ValueError( 

802 f"New definition for StorageClass {storageClass.name} ({storageClass}) " 

803 f"differs from current definition ({existing})" 

804 ) 

805 else: 

806 self._storageClasses[storageClass.name] = storageClass 

807 

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

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

810 

811 Parameters 

812 ---------- 

813 storageClassName : `str` 

814 Name of storage class to remove. 

815 

816 Raises 

817 ------ 

818 KeyError 

819 The named storage class is not registered. 

820 

821 Notes 

822 ----- 

823 This method is intended to simplify testing of StorageClassFactory 

824 functionality and it is not expected to be required for normal usage. 

825 """ 

826 del self._storageClasses[storageClassName]