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

327 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-16 02:09 -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 itertools 

31import logging 

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

33 

34from lsst.utils import doImportType 

35from lsst.utils.classes import Singleton 

36from lsst.utils.introspection import get_full_type_name 

37 

38from .config import Config, ConfigSubset 

39from .configSupport import LookupKey 

40from .storageClassDelegate import StorageClassDelegate 

41 

42log = logging.getLogger(__name__) 

43 

44 

45class StorageClassConfig(ConfigSubset): 

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

47 

48 component = "storageClasses" 

49 defaultConfigFile = "storageClasses.yaml" 

50 

51 

52class StorageClass: 

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

54 

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

75 

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) 

85 

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 

108 

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) 

115 

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 

119 

120 self.name = name 

121 

122 if pytype is None: 

123 pytype = object 

124 

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 

134 

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 

163 

164 @property 

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

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

167 return self._components 

168 

169 @property 

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

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

172 return self._derivedComponents 

173 

174 @property 

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

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

177 return self._converters 

178 

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

184 

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 

201 

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 

232 

233 @property 

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

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

236 return set(self._parameters) 

237 

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 

243 

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 

250 

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 

261 

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

263 """Return all defined components. 

264 

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

266 for the corresponding storage class. 

267 

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 

276 

277 def delegate(self) -> StorageClassDelegate: 

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

279 

280 Returns 

281 ------- 

282 delegate : `StorageClassDelegate` 

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

284 The delegate is constructed with this `StorageClass`. 

285 

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) 

295 

296 def isComposite(self) -> bool: 

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

298 

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 

308 

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

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

311 

312 The names are returned in order of priority. 

313 

314 Returns 

315 ------- 

316 names : `tuple` of `LookupKey` 

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

318 """ 

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

320 

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

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

323 

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

325 

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 

336 

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

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

339 

340 Does not check the values. 

341 

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. 

348 

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 

357 

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

359 # list. 

360 external = set(parameters) 

361 

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

367 

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

370 

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. 

381 

382 Returns 

383 ------- 

384 filtered : `Mapping` 

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

386 

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

395 

396 known = self.knownParameters() 

397 

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 

407 

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

409 

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

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

412 

413 Parameters 

414 ---------- 

415 instance : `object` 

416 Object to check. 

417 

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) 

425 

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. 

429 

430 Parameters 

431 ---------- 

432 other : `StorageClass` 

433 The storage class to check. 

434 

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 

446 

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 

454 

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 

460 

461 if issubclass(other_pytype, self_pytype): 

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

463 return True 

464 

465 for candidate_type in self.converters_by_type: 

466 if issubclass(other_pytype, candidate_type): 

467 return True 

468 return False 

469 

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

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

472 associated with this `StorageClass`. 

473 

474 Parameters 

475 ---------- 

476 incorrect : `object` 

477 An object that might be the incorrect type. 

478 

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. 

485 

486 Raises 

487 ------ 

488 TypeError 

489 Raised if no conversion can be found. 

490 """ 

491 if incorrect is None: 

492 return None 

493 

494 # Possible this is the correct type already. 

495 if self.validateInstance(incorrect): 

496 return incorrect 

497 

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 ) 

514 

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 

519 

520 if self.name != other.name: 

521 return False 

522 

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 

529 

530 # Ensure we have the same component keys in each 

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

532 return False 

533 

534 # Same parameters 

535 if self.parameters != other.parameters: 

536 return False 

537 

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 

542 

543 # If we got to this point everything checks out 

544 return True 

545 

546 def __hash__(self) -> int: 

547 return hash(self.name) 

548 

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 

561 

562 # order is preserved in the dict 

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

564 

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 

571 

572 def __str__(self) -> str: 

573 return self.name 

574 

575 

576class StorageClassFactory(metaclass=Singleton): 

577 """Factory for `StorageClass` instances. 

578 

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. 

584 

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

591 

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

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

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

595 

596 # Always seed with the default config 

597 self.addFromConfig(StorageClassConfig()) 

598 

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) 

601 

602 def __str__(self) -> str: 

603 """Return summary of factory. 

604 

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

612 

613StorageClasses 

614-------------- 

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

616""" 

617 

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

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

620 

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. 

627 

628 Returns 

629 ------- 

630 in : `bool` 

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

632 `StorageClass` is present and identical. 

633 

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 

647 

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

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

650 

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) 

659 

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) 

669 

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 

673 

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} 

677 

678 if "converters" in info: 

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

680 

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) 

689 

690 # Fill in other items 

691 storageClassKwargs[compName] = components 

692 

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

700 

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

702 newStorageClass = newStorageClassType() 

703 self.registerStorageClass(newStorageClass, msg=msg) 

704 

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

710 

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

712 processStorageClass(name, sconfig, context) 

713 

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

719 

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. 

728 

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

738 

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 

742 

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 

759 

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

765 

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

767 

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

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

770 

771 Parameters 

772 ---------- 

773 storageClassName : `str` 

774 Name of the storage class to retrieve. 

775 

776 Returns 

777 ------- 

778 instance : `StorageClass` 

779 Instance of the correct `StorageClass`. 

780 

781 Raises 

782 ------ 

783 KeyError 

784 The requested storage class name is not registered. 

785 """ 

786 return self._storageClasses[storageClassName] 

787 

788 def registerStorageClass(self, storageClass: StorageClass, msg: Optional[str] = None) -> None: 

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

790 

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

792 of the supplied `StorageClass`. 

793 

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. 

800 

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 

817 

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

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

820 

821 Parameters 

822 ---------- 

823 storageClassName : `str` 

824 Name of storage class to remove. 

825 

826 Raises 

827 ------ 

828 KeyError 

829 The named storage class is not registered. 

830 

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]