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

342 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-03 02:30 -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 ( 

33 Any, 

34 Collection, 

35 Dict, 

36 ItemsView, 

37 Iterator, 

38 KeysView, 

39 List, 

40 Mapping, 

41 Optional, 

42 Sequence, 

43 Set, 

44 Tuple, 

45 Type, 

46 Union, 

47 ValuesView, 

48) 

49 

50from lsst.utils import doImportType 

51from lsst.utils.classes import Singleton 

52from lsst.utils.introspection import get_full_type_name 

53 

54from .config import Config, ConfigSubset 

55from .configSupport import LookupKey 

56from .storageClassDelegate import StorageClassDelegate 

57 

58log = logging.getLogger(__name__) 

59 

60 

61class StorageClassConfig(ConfigSubset): 

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

63 

64 component = "storageClasses" 

65 defaultConfigFile = "storageClasses.yaml" 

66 

67 

68class StorageClass: 

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

70 

71 Parameters 

72 ---------- 

73 name : `str` 

74 Name to use for this class. 

75 pytype : `type` or `str` 

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

77 components : `dict`, optional 

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

79 derivedComponents : `dict`, optional 

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

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

82 Parameters understood by this `StorageClass` that can control 

83 reading of data from datastores. 

84 delegate : `str`, optional 

85 Fully qualified name of class supporting assembly and disassembly 

86 of a `pytype` instance. 

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

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

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

90 """ 

91 

92 _cls_name: str = "BaseStorageClass" 

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

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

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

96 _cls_delegate: Optional[str] = None 

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

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

99 defaultDelegate: Type = StorageClassDelegate 

100 defaultDelegateName: str = get_full_type_name(defaultDelegate) 

101 

102 def __init__( 

103 self, 

104 name: Optional[str] = None, 

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

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

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

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

109 delegate: Optional[str] = None, 

110 converters: Optional[Dict[str, str]] = None, 

111 ): 

112 if name is None: 

113 name = self._cls_name 

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

115 pytype = self._cls_pytype 

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

117 components = self._cls_components 

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

119 derivedComponents = self._cls_derivedComponents 

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

121 parameters = self._cls_parameters 

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

123 delegate = self._cls_delegate 

124 

125 # Merge converters with class defaults. 

126 self._converters = {} 

127 if self._cls_converters is not None: 

128 self._converters.update(self._cls_converters) 

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

130 self._converters.update(converters) 

131 

132 # Version of converters where the python types have been 

133 # Do not try to import anything until needed. 

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

135 

136 self.name = name 

137 

138 if pytype is None: 

139 pytype = object 

140 

141 self._pytype: Optional[Type] 

142 if not isinstance(pytype, str): 

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

144 self._pytypeName = get_full_type_name(pytype) 

145 self._pytype = pytype 

146 else: 

147 # Store the type name and defer loading of type 

148 self._pytypeName = pytype 

149 self._pytype = None 

150 

151 if components is not None: 

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

153 raise ValueError( 

154 f"Composite storage class {name} is not allowed to have" 

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

156 " Did you mean it to be a derived component?" 

157 ) 

158 self._components = components 

159 else: 

160 self._components = {} 

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

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

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

164 # delegate 

165 self._delegate: Optional[Type] 

166 self._delegateClassName: Optional[str] 

167 if delegate is not None: 

168 self._delegateClassName = delegate 

169 self._delegate = None 

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

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

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

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

174 self._delegate = self.defaultDelegate 

175 self._delegateClassName = self.defaultDelegateName 

176 else: 

177 self._delegate = None 

178 self._delegateClassName = None 

179 

180 @property 

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

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

183 return self._components 

184 

185 @property 

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

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

188 return self._derivedComponents 

189 

190 @property 

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

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

193 return self._converters 

194 

195 @property 

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

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

198 if self._converters_by_type is None: 

199 self._converters_by_type = {} 

200 

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

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

203 if hasattr(builtins, candidate_type_str): 

204 candidate_type = getattr(builtins, candidate_type_str) 

205 else: 

206 try: 

207 candidate_type = doImportType(candidate_type_str) 

208 except ImportError as e: 

209 log.warning( 

210 "Unable to import type %s associated with storage class %s (%s)", 

211 candidate_type_str, 

212 self.name, 

213 e, 

214 ) 

215 del self.converters[candidate_type_str] 

216 continue 

217 

218 try: 

219 converter = doImportType(converter_str) 

220 except ImportError as e: 

221 log.warning( 

222 "Unable to import conversion function %s associated with storage class %s " 

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

224 converter_str, 

225 self.name, 

226 candidate_type_str, 

227 e, 

228 ) 

229 del self.converters[candidate_type_str] 

230 continue 

231 if not callable(converter): 

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

233 # fact it can return Any except ModuleType because package 

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

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

236 # so we must ignore the warning. 

237 log.warning( # type: ignore 

238 "Conversion function %s associated with storage class " 

239 "%s to convert type %s is not a callable.", 

240 converter_str, 

241 self.name, 

242 candidate_type_str, 

243 ) 

244 del self.converters[candidate_type_str] 

245 continue 

246 self._converters_by_type[candidate_type] = converter 

247 return self._converters_by_type 

248 

249 @property 

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

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

252 return set(self._parameters) 

253 

254 @property 

255 def pytype(self) -> Type: 

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

257 if self._pytype is not None: 

258 return self._pytype 

259 

260 if hasattr(builtins, self._pytypeName): 

261 pytype = getattr(builtins, self._pytypeName) 

262 else: 

263 pytype = doImportType(self._pytypeName) 

264 self._pytype = pytype 

265 return self._pytype 

266 

267 @property 

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

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

270 if self._delegate is not None: 

271 return self._delegate 

272 if self._delegateClassName is None: 

273 return None 

274 delegate_class = doImportType(self._delegateClassName) 

275 self._delegate = delegate_class 

276 return self._delegate 

277 

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

279 """Return all defined components. 

280 

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

282 for the corresponding storage class. 

283 

284 Returns 

285 ------- 

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

287 The component name to storage class mapping. 

288 """ 

289 components = copy.copy(self.components) 

290 components.update(self.derivedComponents) 

291 return components 

292 

293 def delegate(self) -> StorageClassDelegate: 

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

295 

296 Returns 

297 ------- 

298 delegate : `StorageClassDelegate` 

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

300 The delegate is constructed with this `StorageClass`. 

301 

302 Raises 

303 ------ 

304 TypeError 

305 This StorageClass has no associated delegate. 

306 """ 

307 cls = self.delegateClass 

308 if cls is None: 

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

310 return cls(storageClass=self) 

311 

312 def isComposite(self) -> bool: 

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

314 

315 Returns 

316 ------- 

317 isComposite : `bool` 

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

319 otherwise. 

320 """ 

321 if self.components: 

322 return True 

323 return False 

324 

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

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

327 

328 The names are returned in order of priority. 

329 

330 Returns 

331 ------- 

332 names : `tuple` of `LookupKey` 

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

334 """ 

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

336 

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

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

339 

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

341 

342 Returns 

343 ------- 

344 known : `set` 

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

346 storage classes. 

347 """ 

348 known = set(self._parameters) 

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

350 known.update(sc.knownParameters()) 

351 return known 

352 

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

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

355 

356 Does not check the values. 

357 

358 Parameters 

359 ---------- 

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

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

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

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

364 

365 Raises 

366 ------ 

367 KeyError 

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

369 """ 

370 # No parameters is always okay 

371 if not parameters: 

372 return 

373 

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

375 # list. 

376 external = set(parameters) 

377 

378 diff = external - self.knownParameters() 

379 if diff: 

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

381 unknown = "', '".join(diff) 

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

383 

384 def filterParameters(self, parameters: Mapping[str, Any], subset: Collection = None) -> Mapping[str, Any]: 

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

386 

387 Parameters 

388 ---------- 

389 parameters : `Mapping`, optional 

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

391 been provided. 

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

393 Subset of supported parameters that the caller is interested 

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

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

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

397 

398 Returns 

399 ------- 

400 filtered : `Mapping` 

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

402 

403 Raises 

404 ------ 

405 ValueError 

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

407 parameters or if it is an empty set. 

408 """ 

409 if not parameters: 

410 return {} 

411 

412 known = self.knownParameters() 

413 

414 if subset is not None: 

415 if not subset: 

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

417 subset = set(subset) 

418 if not subset.issubset(known): 

419 raise ValueError(f"Requested subset ({subset}) is not a subset of known parameters ({known})") 

420 wanted = subset 

421 else: 

422 wanted = known 

423 

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

425 

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

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

428 

429 Parameters 

430 ---------- 

431 instance : `object` 

432 Object to check. 

433 

434 Returns 

435 ------- 

436 isOk : `bool` 

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

438 `StorageClass`, False otherwise. 

439 """ 

440 return isinstance(instance, self.pytype) 

441 

442 def is_type(self, other: Type) -> bool: 

443 """Return Boolean indicating whether the supplied type matches 

444 the type in this `StorageClass`. 

445 

446 Parameters 

447 ---------- 

448 other : `Type` 

449 The type to be checked. 

450 

451 Returns 

452 ------- 

453 match : `bool` 

454 `True` if the types are equal. 

455 

456 Notes 

457 ----- 

458 If this `StorageClass` has not yet imported the Python type the 

459 check is done against the full type name, this prevents an attempt 

460 to import the type when it will likely not match. 

461 """ 

462 if self._pytype: 

463 return self._pytype is other 

464 

465 other_name = get_full_type_name(other) 

466 return self._pytypeName == other_name 

467 

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

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

470 in the other storage class. 

471 

472 Parameters 

473 ---------- 

474 other : `StorageClass` 

475 The storage class to check. 

476 

477 Returns 

478 ------- 

479 can : `bool` 

480 `True` if this storage class has a registered converter for 

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

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

483 with this storage class. 

484 """ 

485 if other.name == self.name: 

486 # Identical storage classes are compatible. 

487 return True 

488 

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

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

491 # case conversion must be impossible. 

492 try: 

493 other_pytype = other.pytype 

494 except Exception: 

495 return False 

496 

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

498 try: 

499 self_pytype = self.pytype 

500 except Exception: 

501 return False 

502 

503 if issubclass(other_pytype, self_pytype): 

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

505 return True 

506 

507 for candidate_type in self.converters_by_type: 

508 if issubclass(other_pytype, candidate_type): 

509 return True 

510 return False 

511 

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

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

514 associated with this `StorageClass`. 

515 

516 Parameters 

517 ---------- 

518 incorrect : `object` 

519 An object that might be the incorrect type. 

520 

521 Returns 

522 ------- 

523 correct : `object` 

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

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

526 returned. 

527 

528 Raises 

529 ------ 

530 TypeError 

531 Raised if no conversion can be found. 

532 """ 

533 if incorrect is None: 

534 return None 

535 

536 # Possible this is the correct type already. 

537 if self.validateInstance(incorrect): 

538 return incorrect 

539 

540 # Check each registered converter. 

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

542 if isinstance(incorrect, candidate_type): 

543 try: 

544 return converter(incorrect) 

545 except Exception: 

546 log.error( 

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

548 get_full_type_name(converter), 

549 get_full_type_name(incorrect), 

550 ) 

551 raise 

552 raise TypeError( 

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

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

555 ) 

556 

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

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

559 if not isinstance(other, StorageClass): 

560 return NotImplemented 

561 

562 if self.name != other.name: 

563 return False 

564 

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

566 # to trigger an import of external module code here 

567 if self._delegateClassName != other._delegateClassName: 

568 return False 

569 if self._pytypeName != other._pytypeName: 

570 return False 

571 

572 # Ensure we have the same component keys in each 

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

574 return False 

575 

576 # Same parameters 

577 if self.parameters != other.parameters: 

578 return False 

579 

580 # Ensure that all the components have the same type 

581 for k in self.components: 

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

583 return False 

584 

585 # If we got to this point everything checks out 

586 return True 

587 

588 def __hash__(self) -> int: 

589 return hash(self.name) 

590 

591 def __repr__(self) -> str: 

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

593 if self._pytypeName != "object": 

594 optionals["pytype"] = self._pytypeName 

595 if self._delegateClassName is not None: 

596 optionals["delegate"] = self._delegateClassName 

597 if self._parameters: 

598 optionals["parameters"] = self._parameters 

599 if self.components: 

600 optionals["components"] = self.components 

601 if self.converters: 

602 optionals["converters"] = self.converters 

603 

604 # order is preserved in the dict 

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

606 

607 # Start with mandatory fields 

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

609 if options: 

610 r = r + ", " + options 

611 r = r + ")" 

612 return r 

613 

614 def __str__(self) -> str: 

615 return self.name 

616 

617 

618class StorageClassFactory(metaclass=Singleton): 

619 """Factory for `StorageClass` instances. 

620 

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

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

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

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

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

626 

627 Parameters 

628 ---------- 

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

630 Load configuration. In a ButlerConfig` the relevant configuration 

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

632 """ 

633 

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

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

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

637 

638 # Always seed with the default config 

639 self.addFromConfig(StorageClassConfig()) 

640 

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

642 self.addFromConfig(config) 

643 

644 def __str__(self) -> str: 

645 """Return summary of factory. 

646 

647 Returns 

648 ------- 

649 summary : `str` 

650 Summary of the factory status. 

651 """ 

652 sep = "\n" 

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

654 

655StorageClasses 

656-------------- 

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

658""" 

659 

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

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

662 

663 Parameters 

664 ---------- 

665 storageClassOrName : `str` or `StorageClass` 

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

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

668 existence and equality are checked. 

669 

670 Returns 

671 ------- 

672 in : `bool` 

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

674 `StorageClass` is present and identical. 

675 

676 Notes 

677 ----- 

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

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

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

681 in the factory. 

682 """ 

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

684 return storageClassOrName in self._storageClasses 

685 elif isinstance(storageClassOrName, StorageClass): 

686 if storageClassOrName.name in self._storageClasses: 

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

688 return False 

689 

690 def __len__(self) -> int: 

691 return len(self._storageClasses) 

692 

693 def __iter__(self) -> Iterator[str]: 

694 return iter(self._storageClasses) 

695 

696 def values(self) -> ValuesView[StorageClass]: 

697 return self._storageClasses.values() 

698 

699 def keys(self) -> KeysView[str]: 

700 return self._storageClasses.keys() 

701 

702 def items(self) -> ItemsView[str, StorageClass]: 

703 return self._storageClasses.items() 

704 

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

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

707 

708 Parameters 

709 ---------- 

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

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

712 key if part of a global configuration. 

713 """ 

714 sconfig = StorageClassConfig(config) 

715 self._configs.append(sconfig) 

716 

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

718 # components or parents before their classes are defined 

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

720 # to extract definitions from the configuration. 

721 def processStorageClass(name: str, sconfig: StorageClassConfig, msg: str = "") -> None: 

722 # Maybe we've already processed this through recursion 

723 if name not in sconfig: 

724 return 

725 info = sconfig.pop(name) 

726 

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

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

729 components = None 

730 

731 # Extract scalar items from dict that are needed for 

732 # StorageClass Constructor 

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

734 

735 if "converters" in info: 

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

737 

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

739 if compName not in info: 

740 continue 

741 components = {} 

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

743 if ctype not in self: 

744 processStorageClass(ctype, sconfig, msg) 

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

746 

747 # Fill in other items 

748 storageClassKwargs[compName] = components 

749 

750 # Create the new storage class and register it 

751 baseClass = None 

752 if "inheritsFrom" in info: 

753 baseName = info["inheritsFrom"] 

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

755 processStorageClass(baseName, sconfig, msg) 

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

757 

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

759 newStorageClass = newStorageClassType() 

760 self.registerStorageClass(newStorageClass, msg=msg) 

761 

762 # In case there is a problem, construct a context message for any 

763 # error reporting. 

764 files = [str(f) for f in itertools.chain([sconfig.configFile], sconfig.filesRead) if f] 

765 context = f"when adding definitions from {', '.join(files)}" if files else "" 

766 log.debug("Adding definitions from config %s", ", ".join(files)) 

767 

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

769 processStorageClass(name, sconfig, context) 

770 

771 @staticmethod 

772 def makeNewStorageClass( 

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

774 ) -> Type[StorageClass]: 

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

776 

777 Parameters 

778 ---------- 

779 name : `str` 

780 Name to use for this class. 

781 baseClass : `type`, optional 

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

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

784 be used. 

785 

786 Returns 

787 ------- 

788 newtype : `type` subclass of `StorageClass` 

789 Newly created Python type. 

790 """ 

791 if baseClass is None: 

792 baseClass = StorageClass 

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

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

795 

796 # convert the arguments to use different internal names 

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

798 clsargs["_cls_name"] = name 

799 

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

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

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

803 # work consistently. 

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

805 classKey = f"_cls_{k}" 

806 if classKey in clsargs: 

807 baseValue = getattr(baseClass, classKey, None) 

808 if baseValue is not None: 

809 currentValue = clsargs[classKey] 

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

811 newValue = baseValue.copy() 

812 else: 

813 newValue = set(baseValue) 

814 newValue.update(currentValue) 

815 clsargs[classKey] = newValue 

816 

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

818 # parameters in the class can not be modified. 

819 pk = "_cls_parameters" 

820 if pk in clsargs: 

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

822 

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

824 

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

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

827 

828 Parameters 

829 ---------- 

830 storageClassName : `str` 

831 Name of the storage class to retrieve. 

832 

833 Returns 

834 ------- 

835 instance : `StorageClass` 

836 Instance of the correct `StorageClass`. 

837 

838 Raises 

839 ------ 

840 KeyError 

841 The requested storage class name is not registered. 

842 """ 

843 return self._storageClasses[storageClassName] 

844 

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

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

847 

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

849 of the supplied `StorageClass`. 

850 

851 Parameters 

852 ---------- 

853 storageClass : `StorageClass` 

854 Type of the Python `StorageClass` to register. 

855 msg : `str`, optional 

856 Additional message string to be included in any error message. 

857 

858 Raises 

859 ------ 

860 ValueError 

861 If a storage class has already been registered with 

862 that storage class name and the previous definition differs. 

863 """ 

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

865 existing = self.getStorageClass(storageClass.name) 

866 if existing != storageClass: 

867 errmsg = f" {msg}" if msg else "" 

868 raise ValueError( 

869 f"New definition for StorageClass {storageClass.name} ({storageClass!r}) " 

870 f"differs from current definition ({existing!r}){errmsg}" 

871 ) 

872 else: 

873 self._storageClasses[storageClass.name] = storageClass 

874 

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

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

877 

878 Parameters 

879 ---------- 

880 storageClassName : `str` 

881 Name of storage class to remove. 

882 

883 Raises 

884 ------ 

885 KeyError 

886 The named storage class is not registered. 

887 

888 Notes 

889 ----- 

890 This method is intended to simplify testing of StorageClassFactory 

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

892 """ 

893 del self._storageClasses[storageClassName]