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 

31 

32from typing import ( 

33 Any, 

34 Collection, 

35 Dict, 

36 List, 

37 Mapping, 

38 Optional, 

39 Set, 

40 Sequence, 

41 Tuple, 

42 Type, 

43 Union, 

44) 

45 

46from lsst.utils import doImportType 

47from lsst.utils.introspection import get_full_type_name 

48from lsst.utils.classes import Singleton 

49from .storageClassDelegate import StorageClassDelegate 

50from .config import ConfigSubset, Config 

51from .configSupport import LookupKey 

52 

53log = logging.getLogger(__name__) 

54 

55 

56class StorageClassConfig(ConfigSubset): 

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

58 

59 component = "storageClasses" 

60 defaultConfigFile = "storageClasses.yaml" 

61 

62 

63class StorageClass: 

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

65 

66 Parameters 

67 ---------- 

68 name : `str` 

69 Name to use for this class. 

70 pytype : `type` or `str` 

71 Python type (or name of type) to associate with the `StorageClass` 

72 components : `dict`, optional 

73 `dict` mapping name of a component to another `StorageClass`. 

74 derivedComponents : `dict`, optional 

75 `dict` mapping name of a derived component to another `StorageClass`. 

76 parameters : `~collections.abc.Sequence` or `~collections.abc.Set` 

77 Parameters understood by this `StorageClass` that can control 

78 reading of data from datastores. 

79 delegate : `str`, optional 

80 Fully qualified name of class supporting assembly and disassembly 

81 of a `pytype` instance. 

82 converters : `dict` [`str`, `str`], optional 

83 Mapping of python type to function that can be called to convert 

84 that python type to the valid type of this storage class. 

85 """ 

86 

87 _cls_name: str = "BaseStorageClass" 

88 _cls_components: Optional[Dict[str, StorageClass]] = None 

89 _cls_derivedComponents: Optional[Dict[str, StorageClass]] = None 

90 _cls_parameters: Optional[Union[Set[str], Sequence[str]]] = None 

91 _cls_delegate: Optional[str] = None 

92 _cls_pytype: Optional[Union[Type, str]] = None 

93 _cls_converters: Optional[Dict[str, str]] = None 

94 defaultDelegate: Type = StorageClassDelegate 

95 defaultDelegateName: str = get_full_type_name(defaultDelegate) 

96 

97 def __init__(self, name: Optional[str] = None, 

98 pytype: Optional[Union[Type, str]] = None, 

99 components: Optional[Dict[str, StorageClass]] = None, 

100 derivedComponents: Optional[Dict[str, StorageClass]] = None, 

101 parameters: Optional[Union[Sequence, Set]] = None, 

102 delegate: Optional[str] = None, 

103 converters: Optional[Dict[str, str]] = None): 

104 if name is None: 

105 name = self._cls_name 

106 if pytype is None: 106 ↛ 108line 106 didn't jump to line 108, because the condition on line 106 was never false

107 pytype = self._cls_pytype 

108 if components is None: 108 ↛ 110line 108 didn't jump to line 110, because the condition on line 108 was never false

109 components = self._cls_components 

110 if derivedComponents is None: 110 ↛ 112line 110 didn't jump to line 112, because the condition on line 110 was never false

111 derivedComponents = self._cls_derivedComponents 

112 if parameters is None: 112 ↛ 114line 112 didn't jump to line 114, because the condition on line 112 was never false

113 parameters = self._cls_parameters 

114 if delegate is None: 114 ↛ 118line 114 didn't jump to line 118, because the condition on line 114 was never false

115 delegate = self._cls_delegate 

116 

117 # Merge converters with class defaults. 

118 self._converters = {} 

119 if self._cls_converters is not None: 

120 self._converters.update(self._cls_converters) 

121 if converters: 121 ↛ 122line 121 didn't jump to line 122, because the condition on line 121 was never true

122 self._converters.update(converters) 

123 

124 # Version of converters where the python types have been 

125 # Do not try to import anything until needed. 

126 self._converters_by_type: Optional[Dict[Type, Type]] = None 

127 

128 self.name = name 

129 

130 if pytype is None: 

131 pytype = object 

132 

133 self._pytype: Optional[Type] 

134 if not isinstance(pytype, str): 

135 # Already have a type so store it and get the name 

136 self._pytypeName = get_full_type_name(pytype) 

137 self._pytype = pytype 

138 else: 

139 # Store the type name and defer loading of type 

140 self._pytypeName = pytype 

141 self._pytype = None 

142 

143 if components is not None: 

144 if len(components) == 1: 144 ↛ 145line 144 didn't jump to line 145, because the condition on line 144 was never true

145 raise ValueError(f"Composite storage class {name} is not allowed to have" 

146 f" only one component '{next(iter(components))}'." 

147 " Did you mean it to be a derived component?") 

148 self._components = components 

149 else: 

150 self._components = {} 

151 self._derivedComponents = derivedComponents if derivedComponents is not None else {} 

152 self._parameters = frozenset(parameters) if parameters is not None else frozenset() 

153 # if the delegate is not None also set it and clear the default 

154 # delegate 

155 self._delegate: Optional[Type] 

156 self._delegateClassName: Optional[str] 

157 if delegate is not None: 

158 self._delegateClassName = delegate 

159 self._delegate = None 

160 elif components is not None: 160 ↛ 163line 160 didn't jump to line 163, because the condition on line 160 was never true

161 # We set a default delegate for composites so that a class is 

162 # guaranteed to support something if it is a composite. 

163 log.debug("Setting default delegate for %s", self.name) 

164 self._delegate = self.defaultDelegate 

165 self._delegateClassName = self.defaultDelegateName 

166 else: 

167 self._delegate = None 

168 self._delegateClassName = None 

169 

170 @property 

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

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

173 return self._components 

174 

175 @property 

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

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

178 return self._derivedComponents 

179 

180 @property 

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

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

183 return self._converters 

184 

185 @property 

186 def converters_by_type(self) -> Dict[Type, Type]: 

187 """Return the type converters as python types.""" 

188 if self._converters_by_type is None: 

189 self._converters_by_type = {} 

190 

191 # Loop over list because the dict can be edited in loop. 

192 for candidate_type_str, converter_str in list(self.converters.items()): 

193 if hasattr(builtins, candidate_type_str): 

194 candidate_type = getattr(builtins, candidate_type_str) 

195 else: 

196 try: 

197 candidate_type = doImportType(candidate_type_str) 

198 except ImportError as e: 

199 log.warning("Unable to import type %s associated with storage class %s (%s)", 

200 candidate_type_str, self.name, e) 

201 del self.converters[candidate_type_str] 

202 continue 

203 

204 try: 

205 converter = doImportType(converter_str) 

206 except ImportError as e: 

207 log.warning("Unable to import conversion function %s associated with storage class %s " 

208 "required to convert type %s (%s)", 

209 converter_str, self.name, candidate_type_str, e) 

210 del self.converters[candidate_type_str] 

211 continue 

212 if not callable(converter): 

213 # doImportType is annotated to return a Type but in actual 

214 # fact it can return Any except ModuleType because package 

215 # variables can be accessed. This make mypy believe it 

216 # is impossible for the return value to not be a callable 

217 # so we must ignore the warning. 

218 log.warning("Conversion function %s associated with storage class %s to " # type: ignore 

219 "convert type %s is not a callable.", converter_str, self.name, 

220 candidate_type_str) 

221 del self.converters[candidate_type_str] 

222 continue 

223 self._converters_by_type[candidate_type] = converter 

224 return self._converters_by_type 

225 

226 @property 

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

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

229 return set(self._parameters) 

230 

231 @property 

232 def pytype(self) -> Type: 

233 """Return Python type associated with this `StorageClass`.""" 

234 if self._pytype is not None: 

235 return self._pytype 

236 

237 if hasattr(builtins, self._pytypeName): 

238 pytype = getattr(builtins, self._pytypeName) 

239 else: 

240 pytype = doImportType(self._pytypeName) 

241 self._pytype = pytype 

242 return self._pytype 

243 

244 @property 

245 def delegateClass(self) -> Optional[Type]: 

246 """Class to use to delegate type-specific actions.""" 

247 if self._delegate is not None: 

248 return self._delegate 

249 if self._delegateClassName is None: 

250 return None 

251 delegate_class = doImportType(self._delegateClassName) 

252 self._delegate = delegate_class 

253 return self._delegate 

254 

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

256 """Return all defined components. 

257 

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

259 for the corresponding storage class. 

260 

261 Returns 

262 ------- 

263 comp : `dict` of [`str`, `StorageClass`] 

264 The component name to storage class mapping. 

265 """ 

266 components = copy.copy(self.components) 

267 components.update(self.derivedComponents) 

268 return components 

269 

270 def delegate(self) -> StorageClassDelegate: 

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

272 

273 Returns 

274 ------- 

275 delegate : `StorageClassDelegate` 

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

277 The delegate is constructed with this `StorageClass`. 

278 

279 Raises 

280 ------ 

281 TypeError 

282 This StorageClass has no associated delegate. 

283 """ 

284 cls = self.delegateClass 

285 if cls is None: 

286 raise TypeError(f"No delegate class is associated with StorageClass {self.name}") 

287 return cls(storageClass=self) 

288 

289 def isComposite(self) -> bool: 

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

291 

292 Returns 

293 ------- 

294 isComposite : `bool` 

295 `True` if this `StorageClass` is a composite, `False` 

296 otherwise. 

297 """ 

298 if self.components: 

299 return True 

300 return False 

301 

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

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

304 

305 The names are returned in order of priority. 

306 

307 Returns 

308 ------- 

309 names : `tuple` of `LookupKey` 

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

311 """ 

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

313 

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

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

316 

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

318 

319 Returns 

320 ------- 

321 known : `set` 

322 All parameter keys of this `StorageClass` and the component 

323 storage classes. 

324 """ 

325 known = set(self._parameters) 

326 for sc in self.components.values(): 

327 known.update(sc.knownParameters()) 

328 return known 

329 

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

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

332 

333 Does not check the values. 

334 

335 Parameters 

336 ---------- 

337 parameters : `~collections.abc.Collection`, optional 

338 Collection containing the parameters. Can be `dict`-like or 

339 `set`-like. The parameter values are not checked. 

340 If no parameters are supplied, always returns without error. 

341 

342 Raises 

343 ------ 

344 KeyError 

345 Some parameters are not understood by this `StorageClass`. 

346 """ 

347 # No parameters is always okay 

348 if not parameters: 

349 return 

350 

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

352 # list. 

353 external = set(parameters) 

354 

355 diff = external - self.knownParameters() 

356 if diff: 

357 s = "s" if len(diff) > 1 else "" 

358 unknown = '\', \''.join(diff) 

359 raise KeyError(f"Parameter{s} '{unknown}' not understood by StorageClass {self.name}") 

360 

361 def filterParameters(self, parameters: Dict[str, Any], 

362 subset: Collection = None) -> Dict[str, Any]: 

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

364 

365 Parameters 

366 ---------- 

367 parameters : `dict`, optional 

368 Candidate parameters. Can be `None` if no parameters have 

369 been provided. 

370 subset : `~collections.abc.Collection`, optional 

371 Subset of supported parameters that the caller is interested 

372 in using. The subset must be known to the `StorageClass` 

373 if specified. If `None` the supplied parameters will all 

374 be checked, else only the keys in this set will be checked. 

375 

376 Returns 

377 ------- 

378 filtered : `dict` 

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

380 

381 Raises 

382 ------ 

383 ValueError 

384 Raised if the provided subset is not a subset of the supported 

385 parameters or if it is an empty set. 

386 """ 

387 if not parameters: 

388 return {} 

389 

390 known = self.knownParameters() 

391 

392 if subset is not None: 

393 if not subset: 

394 raise ValueError("Specified a parameter subset but it was empty") 

395 subset = set(subset) 

396 if not subset.issubset(known): 

397 raise ValueError(f"Requested subset ({subset}) is not a subset of" 

398 f" known parameters ({known})") 

399 wanted = subset 

400 else: 

401 wanted = known 

402 

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

404 

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

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

407 

408 Parameters 

409 ---------- 

410 instance : `object` 

411 Object to check. 

412 

413 Returns 

414 ------- 

415 isOk : `bool` 

416 True if the supplied instance object can be handled by this 

417 `StorageClass`, False otherwise. 

418 """ 

419 return isinstance(instance, self.pytype) 

420 

421 def can_convert(self, other: StorageClass) -> bool: 

422 """Return `True` if this storage class can convert python types 

423 in the other storage class. 

424 

425 Parameters 

426 ---------- 

427 other : `StorageClass` 

428 The storage class to check. 

429 

430 Returns 

431 ------- 

432 can : `bool` 

433 `True` if the two storage classes are compatible. 

434 """ 

435 if other.name == self.name: 

436 # Identical storage classes are compatible. 

437 return True 

438 

439 for candidate_type in self.converters_by_type: 

440 if issubclass(other.pytype, candidate_type): 

441 return True 

442 return False 

443 

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

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

446 associated with this `StorageClass`. 

447 

448 Parameters 

449 ---------- 

450 incorrect : `object` 

451 An object that might be the incorrect type. 

452 

453 Returns 

454 ------- 

455 correct : `object` 

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

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

458 returned. 

459 

460 Raises 

461 ------ 

462 TypeError 

463 Raised if no conversion can be found. 

464 """ 

465 if incorrect is None: 

466 return None 

467 

468 # Possible this is the correct type already. 

469 if self.validateInstance(incorrect): 

470 return incorrect 

471 

472 # Check each registered converter. 

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

474 if isinstance(incorrect, candidate_type): 

475 try: 

476 return converter(incorrect) 

477 except Exception: 

478 log.error("Converter %s failed to convert type %s", 

479 get_full_type_name(converter), get_full_type_name(incorrect)) 

480 raise 

481 raise TypeError("Type does not match and no valid converter found to convert" 

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

483 

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

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

486 if not isinstance(other, StorageClass): 

487 return NotImplemented 

488 

489 if self.name != other.name: 

490 return False 

491 

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

493 # to trigger an import of external module code here 

494 if self._delegateClassName != other._delegateClassName: 

495 return False 

496 if self._pytypeName != other._pytypeName: 

497 return False 

498 

499 # Ensure we have the same component keys in each 

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

501 return False 

502 

503 # Same parameters 

504 if self.parameters != other.parameters: 

505 return False 

506 

507 # Ensure that all the components have the same type 

508 for k in self.components: 

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

510 return False 

511 

512 # If we got to this point everything checks out 

513 return True 

514 

515 def __hash__(self) -> int: 

516 return hash(self.name) 

517 

518 def __repr__(self) -> str: 

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

520 if self._pytypeName != "object": 

521 optionals["pytype"] = self._pytypeName 

522 if self._delegateClassName is not None: 

523 optionals["delegate"] = self._delegateClassName 

524 if self._parameters: 

525 optionals["parameters"] = self._parameters 

526 if self.components: 

527 optionals["components"] = self.components 

528 if self.converters: 

529 optionals["converters"] = self.converters 

530 

531 # order is preserved in the dict 

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

533 

534 # Start with mandatory fields 

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

536 if options: 

537 r = r + ", " + options 

538 r = r + ")" 

539 return r 

540 

541 def __str__(self) -> str: 

542 return self.name 

543 

544 

545class StorageClassFactory(metaclass=Singleton): 

546 """Factory for `StorageClass` instances. 

547 

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

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

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

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

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

553 

554 Parameters 

555 ---------- 

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

557 Load configuration. In a ButlerConfig` the relevant configuration 

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

559 """ 

560 

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

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

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

564 

565 # Always seed with the default config 

566 self.addFromConfig(StorageClassConfig()) 

567 

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

569 self.addFromConfig(config) 

570 

571 def __str__(self) -> str: 

572 """Return summary of factory. 

573 

574 Returns 

575 ------- 

576 summary : `str` 

577 Summary of the factory status. 

578 """ 

579 sep = "\n" 

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

581 

582StorageClasses 

583-------------- 

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

585""" 

586 

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

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

589 

590 Parameters 

591 ---------- 

592 storageClassOrName : `str` or `StorageClass` 

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

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

595 existence and equality are checked. 

596 

597 Returns 

598 ------- 

599 in : `bool` 

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

601 `StorageClass` is present and identical. 

602 

603 Notes 

604 ----- 

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

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

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

608 in the factory. 

609 """ 

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

611 return storageClassOrName in self._storageClasses 

612 elif isinstance(storageClassOrName, StorageClass): 

613 if storageClassOrName.name in self._storageClasses: 

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

615 return False 

616 

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

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

619 

620 Parameters 

621 ---------- 

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

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

624 key if part of a global configuration. 

625 """ 

626 sconfig = StorageClassConfig(config) 

627 self._configs.append(sconfig) 

628 

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

630 # components or parents before their classes are defined 

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

632 # to extract definitions from the configuration. 

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

634 # Maybe we've already processed this through recursion 

635 if name not in sconfig: 

636 return 

637 info = sconfig.pop(name) 

638 

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

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

641 components = None 

642 

643 # Extract scalar items from dict that are needed for 

644 # StorageClass Constructor 

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

646 

647 if "converters" in info: 

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

649 

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

651 if compName not in info: 

652 continue 

653 components = {} 

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

655 if ctype not in self: 

656 processStorageClass(ctype, sconfig) 

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

658 

659 # Fill in other items 

660 storageClassKwargs[compName] = components 

661 

662 # Create the new storage class and register it 

663 baseClass = None 

664 if "inheritsFrom" in info: 

665 baseName = info["inheritsFrom"] 

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

667 processStorageClass(baseName, sconfig) 

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

669 

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

671 newStorageClass = newStorageClassType() 

672 self.registerStorageClass(newStorageClass) 

673 

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

675 processStorageClass(name, sconfig) 

676 

677 @staticmethod 

678 def makeNewStorageClass(name: str, 

679 baseClass: Optional[Type[StorageClass]] = StorageClass, 

680 **kwargs: Any) -> Type[StorageClass]: 

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

682 

683 Parameters 

684 ---------- 

685 name : `str` 

686 Name to use for this class. 

687 baseClass : `type`, optional 

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

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

690 be used. 

691 

692 Returns 

693 ------- 

694 newtype : `type` subclass of `StorageClass` 

695 Newly created Python type. 

696 """ 

697 if baseClass is None: 

698 baseClass = StorageClass 

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

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

701 

702 # convert the arguments to use different internal names 

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

704 clsargs["_cls_name"] = name 

705 

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

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

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

709 # work consistently. 

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

711 classKey = f"_cls_{k}" 

712 if classKey in clsargs: 

713 baseValue = getattr(baseClass, classKey, None) 

714 if baseValue is not None: 

715 currentValue = clsargs[classKey] 

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

717 newValue = baseValue.copy() 

718 else: 

719 newValue = set(baseValue) 

720 newValue.update(currentValue) 

721 clsargs[classKey] = newValue 

722 

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

724 # parameters in the class can not be modified. 

725 pk = "_cls_parameters" 

726 if pk in clsargs: 

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

728 

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

730 

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

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

733 

734 Parameters 

735 ---------- 

736 storageClassName : `str` 

737 Name of the storage class to retrieve. 

738 

739 Returns 

740 ------- 

741 instance : `StorageClass` 

742 Instance of the correct `StorageClass`. 

743 

744 Raises 

745 ------ 

746 KeyError 

747 The requested storage class name is not registered. 

748 """ 

749 return self._storageClasses[storageClassName] 

750 

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

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

753 

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

755 of the supplied `StorageClass`. 

756 

757 Parameters 

758 ---------- 

759 storageClass : `StorageClass` 

760 Type of the Python `StorageClass` to register. 

761 

762 Raises 

763 ------ 

764 ValueError 

765 If a storage class has already been registered with 

766 storageClassName and the previous definition differs. 

767 """ 

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

769 existing = self.getStorageClass(storageClass.name) 

770 if existing != storageClass: 

771 raise ValueError(f"New definition for StorageClass {storageClass.name} ({storageClass}) " 

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

773 else: 

774 self._storageClasses[storageClass.name] = storageClass 

775 

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

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

778 

779 Parameters 

780 ---------- 

781 storageClassName : `str` 

782 Name of storage class to remove. 

783 

784 Raises 

785 ------ 

786 KeyError 

787 The named storage class is not registered. 

788 

789 Notes 

790 ----- 

791 This method is intended to simplify testing of StorageClassFactory 

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

793 """ 

794 del self._storageClasses[storageClassName]