Hide keyboard shortcuts

Hot-keys 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

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__all__ = ("Formatter", "FormatterFactory", "FormatterParameter") 

25 

26from abc import ABCMeta, abstractmethod 

27from collections.abc import Mapping 

28import contextlib 

29import logging 

30import copy 

31from typing import ( 

32 AbstractSet, 

33 Any, 

34 ClassVar, 

35 Dict, 

36 Iterator, 

37 Optional, 

38 Set, 

39 Tuple, 

40 Type, 

41 TYPE_CHECKING, 

42 Union, 

43) 

44 

45from .configSupport import processLookupConfigs, LookupKey 

46from .mappingFactory import MappingFactory 

47from .utils import getFullTypeName 

48from .fileDescriptor import FileDescriptor 

49from .location import Location 

50from .config import Config 

51from .dimensions import DimensionUniverse 

52from .storageClass import StorageClass 

53from .datasets import DatasetType, DatasetRef 

54 

55log = logging.getLogger(__name__) 

56 

57# Define a new special type for functions that take "entity" 

58Entity = Union[DatasetType, DatasetRef, StorageClass, str] 

59 

60 

61if TYPE_CHECKING: 61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true

62 from .dimensions import DataCoordinate 

63 

64 

65class Formatter(metaclass=ABCMeta): 

66 """Interface for reading and writing Datasets with a particular 

67 `StorageClass`. 

68 

69 Parameters 

70 ---------- 

71 fileDescriptor : `FileDescriptor`, optional 

72 Identifies the file to read or write, and the associated storage 

73 classes and parameter information. Its value can be `None` if the 

74 caller will never call `Formatter.read` or `Formatter.write`. 

75 dataId : `DataCoordinate` 

76 Data ID associated with this formatter. 

77 writeParameters : `dict`, optional 

78 Any parameters to be hard-coded into this instance to control how 

79 the dataset is serialized. 

80 writeRecipes : `dict`, optional 

81 Detailed write Recipes indexed by recipe name. 

82 

83 Notes 

84 ----- 

85 All Formatter subclasses should share the base class's constructor 

86 signature. 

87 """ 

88 

89 unsupportedParameters: ClassVar[Optional[AbstractSet[str]]] = frozenset() 

90 """Set of read parameters not understood by this `Formatter`. An empty set 

91 means all parameters are supported. `None` indicates that no parameters 

92 are supported. These param (`frozenset`). 

93 """ 

94 

95 supportedWriteParameters: ClassVar[Optional[AbstractSet[str]]] = None 

96 """Parameters understood by this formatter that can be used to control 

97 how a dataset is serialized. `None` indicates that no parameters are 

98 supported.""" 

99 

100 supportedExtensions: ClassVar[AbstractSet[str]] = frozenset() 

101 """Set of all extensions supported by this formatter. 

102 

103 Only expected to be populated by Formatters that write files. Any extension 

104 assigned to the ``extension`` property will be automatically included in 

105 the list of supported extensions.""" 

106 

107 def __init__(self, fileDescriptor: FileDescriptor, dataId: DataCoordinate, 

108 writeParameters: Optional[Dict[str, Any]] = None, 

109 writeRecipes: Optional[Dict[str, Any]] = None): 

110 if not isinstance(fileDescriptor, FileDescriptor): 

111 raise TypeError("File descriptor must be a FileDescriptor") 

112 assert dataId is not None, "dataId is now required for formatter initialization" 

113 self._fileDescriptor = fileDescriptor 

114 self._dataId = dataId 

115 

116 # Check that the write parameters are allowed 

117 if writeParameters: 

118 if self.supportedWriteParameters is None: 

119 raise ValueError("This formatter does not accept any write parameters. " 

120 f"Got: {', '.join(writeParameters)}") 

121 else: 

122 given = set(writeParameters) 

123 unknown = given - self.supportedWriteParameters 

124 if unknown: 

125 s = "s" if len(unknown) != 1 else "" 

126 unknownStr = ", ".join(f"'{u}'" for u in unknown) 

127 raise ValueError(f"This formatter does not accept parameter{s} {unknownStr}") 

128 

129 self._writeParameters = writeParameters 

130 self._writeRecipes = self.validateWriteRecipes(writeRecipes) 

131 

132 def __str__(self) -> str: 

133 return f"{self.name()}@{self.fileDescriptor.location.path}" 

134 

135 def __repr__(self) -> str: 

136 return f"{self.name()}({self.fileDescriptor!r})" 

137 

138 @property 

139 def fileDescriptor(self) -> FileDescriptor: 

140 """FileDescriptor associated with this formatter 

141 (`FileDescriptor`, read-only)""" 

142 return self._fileDescriptor 

143 

144 @property 

145 def dataId(self) -> DataCoordinate: 

146 """DataId associated with this formatter (`DataCoordinate`)""" 

147 return self._dataId 

148 

149 @property 

150 def writeParameters(self) -> Mapping[str, Any]: 

151 """Parameters to use when writing out datasets.""" 

152 if self._writeParameters is not None: 

153 return self._writeParameters 

154 return {} 

155 

156 @property 

157 def writeRecipes(self) -> Mapping[str, Any]: 

158 """Detailed write Recipes indexed by recipe name.""" 

159 if self._writeRecipes is not None: 

160 return self._writeRecipes 

161 return {} 

162 

163 @classmethod 

164 def validateWriteRecipes(cls, recipes: Optional[Mapping[str, Any]]) -> Optional[Mapping[str, Any]]: 

165 """Validate supplied recipes for this formatter. 

166 

167 The recipes are supplemented with default values where appropriate. 

168 

169 Parameters 

170 ---------- 

171 recipes : `dict` 

172 Recipes to validate. 

173 

174 Returns 

175 ------- 

176 validated : `dict` 

177 Validated recipes. 

178 

179 Raises 

180 ------ 

181 RuntimeError 

182 Raised if validation fails. The default implementation raises 

183 if any recipes are given. 

184 """ 

185 if recipes: 

186 raise RuntimeError(f"This formatter does not understand these writeRecipes: {recipes}") 

187 return recipes 

188 

189 @classmethod 

190 def name(cls) -> str: 

191 """Returns the fully qualified name of the formatter. 

192 

193 Returns 

194 ------- 

195 name : `str` 

196 Fully-qualified name of formatter class. 

197 """ 

198 return getFullTypeName(cls) 

199 

200 @abstractmethod 

201 def read(self, component: Optional[str] = None) -> Any: 

202 """Read a Dataset. 

203 

204 Parameters 

205 ---------- 

206 component : `str`, optional 

207 Component to read from the file. Only used if the `StorageClass` 

208 for reading differed from the `StorageClass` used to write the 

209 file. 

210 

211 Returns 

212 ------- 

213 inMemoryDataset : `object` 

214 The requested Dataset. 

215 """ 

216 raise NotImplementedError("Type does not support reading") 

217 

218 @abstractmethod 

219 def write(self, inMemoryDataset: Any) -> None: 

220 """Write a Dataset. 

221 

222 Parameters 

223 ---------- 

224 inMemoryDataset : `object` 

225 The Dataset to store. 

226 """ 

227 raise NotImplementedError("Type does not support writing") 

228 

229 @classmethod 

230 def can_read_bytes(cls) -> bool: 

231 """Indicate if this formatter can format from bytes. 

232 

233 Returns 

234 ------- 

235 can : `bool` 

236 `True` if the `fromBytes` method is implemented. 

237 """ 

238 # We have no property to read so instead try to format from a byte 

239 # and see what happens 

240 try: 

241 # We know the arguments are incompatible 

242 cls.fromBytes(cls, b"") # type: ignore 

243 except NotImplementedError: 

244 return False 

245 except Exception: 

246 # There will be problems with the bytes we are supplying so ignore 

247 pass 

248 return True 

249 

250 def fromBytes(self, serializedDataset: bytes, 

251 component: Optional[str] = None) -> object: 

252 """Reads serialized data into a Dataset or its component. 

253 

254 Parameters 

255 ---------- 

256 serializedDataset : `bytes` 

257 Bytes object to unserialize. 

258 component : `str`, optional 

259 Component to read from the Dataset. Only used if the `StorageClass` 

260 for reading differed from the `StorageClass` used to write the 

261 file. 

262 

263 Returns 

264 ------- 

265 inMemoryDataset : `object` 

266 The requested data as a Python object. The type of object 

267 is controlled by the specific formatter. 

268 """ 

269 raise NotImplementedError("Type does not support reading from bytes.") 

270 

271 def toBytes(self, inMemoryDataset: Any) -> bytes: 

272 """Serialize the Dataset to bytes based on formatter. 

273 

274 Parameters 

275 ---------- 

276 inMemoryDataset : `object` 

277 The Python object to serialize. 

278 

279 Returns 

280 ------- 

281 serializedDataset : `bytes` 

282 Bytes representing the serialized dataset. 

283 """ 

284 raise NotImplementedError("Type does not support writing to bytes.") 

285 

286 @contextlib.contextmanager 

287 def _updateLocation(self, location: Optional[Location]) -> Iterator[Location]: 

288 """Temporarily replace the location associated with this formatter. 

289 

290 Parameters 

291 ---------- 

292 location : `Location` 

293 New location to use for this formatter. If `None` the 

294 formatter will not change but it will still return 

295 the old location. This allows it to be used in a code 

296 path where the location may not need to be updated 

297 but the with block is still convenient. 

298 

299 Yields 

300 ------ 

301 old : `Location` 

302 The old location that will be restored. 

303 

304 Notes 

305 ----- 

306 This is an internal method that should be used with care. 

307 It may change in the future. Should be used as a context 

308 manager to restore the location when the temporary is no 

309 longer required. 

310 """ 

311 old = self._fileDescriptor.location 

312 try: 

313 if location is not None: 

314 self._fileDescriptor.location = location 

315 yield old 

316 finally: 

317 if location is not None: 

318 self._fileDescriptor.location = old 

319 

320 def makeUpdatedLocation(self, location: Location) -> Location: 

321 """Return a new `Location` instance updated with this formatter's 

322 extension. 

323 

324 Parameters 

325 ---------- 

326 location : `Location` 

327 The location to update. 

328 

329 Returns 

330 ------- 

331 updated : `Location` 

332 A new `Location` with a new file extension applied. 

333 

334 Raises 

335 ------ 

336 NotImplementedError 

337 Raised if there is no ``extension`` attribute associated with 

338 this formatter. 

339 

340 Notes 

341 ----- 

342 This method is available to all Formatters but might not be 

343 implemented by all formatters. It requires that a formatter set 

344 an ``extension`` attribute containing the file extension used when 

345 writing files. If ``extension`` is `None` the supplied file will 

346 not be updated. Not all formatters write files so this is not 

347 defined in the base class. 

348 """ 

349 location = copy.deepcopy(location) 

350 try: 

351 # We are deliberately allowing extension to be undefined by 

352 # default in the base class and mypy complains. 

353 location.updateExtension(self.extension) # type:ignore 

354 except AttributeError: 

355 raise NotImplementedError("No file extension registered with this formatter") from None 

356 return location 

357 

358 @classmethod 

359 def validateExtension(cls, location: Location) -> None: 

360 """Check that the provided location refers to a file extension that is 

361 understood by this formatter. 

362 

363 Parameters 

364 ---------- 

365 location : `Location` 

366 Location from which to extract a file extension. 

367 

368 Raises 

369 ------ 

370 NotImplementedError 

371 Raised if file extensions are a concept not understood by this 

372 formatter. 

373 ValueError 

374 Raised if the formatter does not understand this extension. 

375 

376 Notes 

377 ----- 

378 This method is available to all Formatters but might not be 

379 implemented by all formatters. It requires that a formatter set 

380 an ``extension`` attribute containing the file extension used when 

381 writing files. If ``extension`` is `None` only the set of supported 

382 extensions will be examined. 

383 """ 

384 supported = set(cls.supportedExtensions) 

385 

386 try: 

387 # We are deliberately allowing extension to be undefined by 

388 # default in the base class and mypy complains. 

389 default = cls.extension # type: ignore 

390 except AttributeError: 

391 raise NotImplementedError("No file extension registered with this formatter") from None 

392 

393 # If extension is implemented as an instance property it won't return 

394 # a string when called as a class propertt. Assume that 

395 # the supported extensions class property is complete. 

396 if default is not None and isinstance(default, str): 

397 supported.add(default) 

398 

399 # Get the file name from the uri 

400 file = location.uri.basename() 

401 

402 # Check that this file name ends with one of the supported extensions. 

403 # This is less prone to confusion than asking the location for 

404 # its extension and then doing a set comparison 

405 for ext in supported: 

406 if file.endswith(ext): 

407 return 

408 

409 raise ValueError(f"Extension '{location.getExtension()}' on '{location}' " 

410 f"is not supported by Formatter '{cls.__name__}' (supports: {supported})") 

411 

412 def predictPath(self) -> str: 

413 """Return the path that would be returned by write, without actually 

414 writing. 

415 

416 Uses the `FileDescriptor` associated with the instance. 

417 

418 Returns 

419 ------- 

420 path : `str` 

421 Path within datastore that would be associated with the location 

422 stored in this `Formatter`. 

423 """ 

424 updated = self.makeUpdatedLocation(self.fileDescriptor.location) 

425 return updated.pathInStore.path 

426 

427 def segregateParameters(self, parameters: Optional[Dict[str, Any]] = None) -> Tuple[Dict, Dict]: 

428 """Segregate the supplied parameters into those understood by the 

429 formatter and those not understood by the formatter. 

430 

431 Any unsupported parameters are assumed to be usable by associated 

432 assemblers. 

433 

434 Parameters 

435 ---------- 

436 parameters : `dict`, optional 

437 Parameters with values that have been supplied by the caller 

438 and which might be relevant for the formatter. If `None` 

439 parameters will be read from the registered `FileDescriptor`. 

440 

441 Returns 

442 ------- 

443 supported : `dict` 

444 Those parameters supported by this formatter. 

445 unsupported : `dict` 

446 Those parameters not supported by this formatter. 

447 """ 

448 

449 if parameters is None: 

450 parameters = self.fileDescriptor.parameters 

451 

452 if parameters is None: 

453 return {}, {} 

454 

455 if self.unsupportedParameters is None: 

456 # Support none of the parameters 

457 return {}, parameters.copy() 

458 

459 # Start by assuming all are supported 

460 supported = parameters.copy() 

461 unsupported = {} 

462 

463 # And remove any we know are not supported 

464 for p in set(supported): 

465 if p in self.unsupportedParameters: 

466 unsupported[p] = supported.pop(p) 

467 

468 return supported, unsupported 

469 

470 

471class FormatterFactory: 

472 """Factory for `Formatter` instances. 

473 """ 

474 

475 defaultKey = LookupKey("default") 

476 """Configuration key associated with default write parameter settings.""" 

477 

478 writeRecipesKey = LookupKey("write_recipes") 

479 """Configuration key associated with write recipes.""" 

480 

481 def __init__(self) -> None: 

482 self._mappingFactory = MappingFactory(Formatter) 

483 

484 def __contains__(self, key: Union[LookupKey, str]) -> bool: 

485 """Indicates whether the supplied key is present in the factory. 

486 

487 Parameters 

488 ---------- 

489 key : `LookupKey`, `str` or objects with ``name`` attribute 

490 Key to use to lookup in the factory whether a corresponding 

491 formatter is present. 

492 

493 Returns 

494 ------- 

495 in : `bool` 

496 `True` if the supplied key is present in the factory. 

497 """ 

498 return key in self._mappingFactory 

499 

500 def registerFormatters(self, config: Config, *, universe: DimensionUniverse) -> None: 

501 """Bulk register formatters from a config. 

502 

503 Parameters 

504 ---------- 

505 config : `Config` 

506 ``formatters`` section of a configuration. 

507 universe : `DimensionUniverse`, optional 

508 Set of all known dimensions, used to expand and validate any used 

509 in lookup keys. 

510 

511 Notes 

512 ----- 

513 The configuration can include one level of hierarchy where an 

514 instrument-specific section can be defined to override more general 

515 template specifications. This is represented in YAML using a 

516 key of form ``instrument<name>`` which can then define templates 

517 that will be returned if a `DatasetRef` contains a matching instrument 

518 name in the data ID. 

519 

520 The config is parsed using the function 

521 `~lsst.daf.butler.configSubset.processLookupConfigs`. 

522 

523 The values for formatter entries can be either a simple string 

524 referring to a python type or a dict representing the formatter and 

525 parameters to be hard-coded into the formatter constructor. For 

526 the dict case the following keys are supported: 

527 

528 - formatter: The python type to be used as the formatter class. 

529 - parameters: A further dict to be passed directly to the 

530 ``writeParameters`` Formatter constructor to seed it. 

531 These parameters are validated at instance creation and not at 

532 configuration. 

533 

534 Additionally, a special ``default`` section can be defined that 

535 uses the formatter type (class) name as the keys and specifies 

536 default write parameters that should be used whenever an instance 

537 of that class is constructed. 

538 

539 .. code-block:: yaml 

540 

541 formatters: 

542 default: 

543 lsst.daf.butler.formatters.example.ExampleFormatter: 

544 max: 10 

545 min: 2 

546 comment: Default comment 

547 calexp: lsst.daf.butler.formatters.example.ExampleFormatter 

548 coadd: 

549 formatter: lsst.daf.butler.formatters.example.ExampleFormatter 

550 parameters: 

551 max: 5 

552 

553 Any time an ``ExampleFormatter`` is constructed it will use those 

554 parameters. If an explicit entry later in the configuration specifies 

555 a different set of parameters, the two will be merged with the later 

556 entry taking priority. In the example above ``calexp`` will use 

557 the default parameters but ``coadd`` will override the value for 

558 ``max``. 

559 

560 Formatter configuration can also include a special section describing 

561 collections of write parameters that can be accessed through a 

562 simple label. This allows common collections of options to be 

563 specified in one place in the configuration and reused later. 

564 The ``write_recipes`` section is indexed by Formatter class name 

565 and each key is the label to associate with the parameters. 

566 

567 .. code-block:: yaml 

568 

569 formatters: 

570 write_recipes: 

571 lsst.obs.base.formatters.fitsExposure.FixExposureFormatter: 

572 lossless: 

573 ... 

574 noCompression: 

575 ... 

576 

577 By convention a formatter that uses write recipes will support a 

578 ``recipe`` write parameter that will refer to a recipe name in 

579 the ``write_recipes`` component. The `Formatter` will be constructed 

580 in the `FormatterFactory` with all the relevant recipes and 

581 will not attempt to filter by looking at ``writeParameters`` in 

582 advance. See the specific formatter documentation for details on 

583 acceptable recipe options. 

584 """ 

585 allowed_keys = {"formatter", "parameters"} 

586 

587 contents = processLookupConfigs(config, allow_hierarchy=True, universe=universe) 

588 

589 # Extract any default parameter settings 

590 defaultParameters = contents.get(self.defaultKey, {}) 

591 if not isinstance(defaultParameters, Mapping): 

592 raise RuntimeError("Default formatter parameters in config can not be a single string" 

593 f" (got: {type(defaultParameters)})") 

594 

595 # Extract any global write recipes -- these are indexed by 

596 # Formatter class name. 

597 writeRecipes = contents.get(self.writeRecipesKey, {}) 

598 if isinstance(writeRecipes, str): 

599 raise RuntimeError(f"The formatters.{self.writeRecipesKey} section must refer to a dict" 

600 f" not '{writeRecipes}'") 

601 

602 for key, f in contents.items(): 

603 # default is handled in a special way 

604 if key == self.defaultKey: 

605 continue 

606 if key == self.writeRecipesKey: 

607 continue 

608 

609 # Can be a str or a dict. 

610 specificWriteParameters = {} 

611 if isinstance(f, str): 

612 formatter = f 

613 elif isinstance(f, Mapping): 

614 all_keys = set(f) 

615 unexpected_keys = all_keys - allowed_keys 

616 if unexpected_keys: 

617 raise ValueError(f"Formatter {key} uses unexpected keys {unexpected_keys} in config") 

618 if "formatter" not in f: 

619 raise ValueError(f"Mandatory 'formatter' key missing for formatter key {key}") 

620 formatter = f["formatter"] 

621 if "parameters" in f: 

622 specificWriteParameters = f["parameters"] 

623 else: 

624 raise ValueError(f"Formatter for key {key} has unexpected value: '{f}'") 

625 

626 # Apply any default parameters for this formatter 

627 writeParameters = copy.deepcopy(defaultParameters.get(formatter, {})) 

628 writeParameters.update(specificWriteParameters) 

629 

630 kwargs: Dict[str, Any] = {} 

631 if writeParameters: 

632 kwargs["writeParameters"] = writeParameters 

633 

634 if formatter in writeRecipes: 

635 kwargs["writeRecipes"] = writeRecipes[formatter] 

636 

637 self.registerFormatter(key, formatter, **kwargs) 

638 

639 def getLookupKeys(self) -> Set[LookupKey]: 

640 """Retrieve the look up keys for all the registry entries. 

641 

642 Returns 

643 ------- 

644 keys : `set` of `LookupKey` 

645 The keys available for matching in the registry. 

646 """ 

647 return self._mappingFactory.getLookupKeys() 

648 

649 def getFormatterClassWithMatch(self, entity: Entity) -> Tuple[LookupKey, Type[Formatter], 

650 Dict[str, Any]]: 

651 """Get the matching formatter class along with the matching registry 

652 key. 

653 

654 Parameters 

655 ---------- 

656 entity : `DatasetRef`, `DatasetType`, `StorageClass`, or `str` 

657 Entity to use to determine the formatter to return. 

658 `StorageClass` will be used as a last resort if `DatasetRef` 

659 or `DatasetType` instance is provided. Supports instrument 

660 override if a `DatasetRef` is provided configured with an 

661 ``instrument`` value for the data ID. 

662 

663 Returns 

664 ------- 

665 matchKey : `LookupKey` 

666 The key that resulted in the successful match. 

667 formatter : `type` 

668 The class of the registered formatter. 

669 formatter_kwargs : `dict` 

670 Keyword arguments that are associated with this formatter entry. 

671 """ 

672 names = (LookupKey(name=entity),) if isinstance(entity, str) else entity._lookupNames() 

673 matchKey, formatter, formatter_kwargs = self._mappingFactory.getClassFromRegistryWithMatch(names) 

674 log.debug("Retrieved formatter %s from key '%s' for entity '%s'", getFullTypeName(formatter), 

675 matchKey, entity) 

676 

677 return matchKey, formatter, formatter_kwargs 

678 

679 def getFormatterClass(self, entity: Entity) -> Type: 

680 """Get the matching formatter class. 

681 

682 Parameters 

683 ---------- 

684 entity : `DatasetRef`, `DatasetType`, `StorageClass`, or `str` 

685 Entity to use to determine the formatter to return. 

686 `StorageClass` will be used as a last resort if `DatasetRef` 

687 or `DatasetType` instance is provided. Supports instrument 

688 override if a `DatasetRef` is provided configured with an 

689 ``instrument`` value for the data ID. 

690 

691 Returns 

692 ------- 

693 formatter : `type` 

694 The class of the registered formatter. 

695 """ 

696 _, formatter, _ = self.getFormatterClassWithMatch(entity) 

697 return formatter 

698 

699 def getFormatterWithMatch(self, entity: Entity, *args: Any, **kwargs: Any) -> Tuple[LookupKey, Formatter]: 

700 """Get a new formatter instance along with the matching registry 

701 key. 

702 

703 Parameters 

704 ---------- 

705 entity : `DatasetRef`, `DatasetType`, `StorageClass`, or `str` 

706 Entity to use to determine the formatter to return. 

707 `StorageClass` will be used as a last resort if `DatasetRef` 

708 or `DatasetType` instance is provided. Supports instrument 

709 override if a `DatasetRef` is provided configured with an 

710 ``instrument`` value for the data ID. 

711 args : `tuple` 

712 Positional arguments to use pass to the object constructor. 

713 kwargs : `dict` 

714 Keyword arguments to pass to object constructor. 

715 

716 Returns 

717 ------- 

718 matchKey : `LookupKey` 

719 The key that resulted in the successful match. 

720 formatter : `Formatter` 

721 An instance of the registered formatter. 

722 """ 

723 names = (LookupKey(name=entity),) if isinstance(entity, str) else entity._lookupNames() 

724 matchKey, formatter = self._mappingFactory.getFromRegistryWithMatch(names, *args, **kwargs) 

725 log.debug("Retrieved formatter %s from key '%s' for entity '%s'", getFullTypeName(formatter), 

726 matchKey, entity) 

727 

728 return matchKey, formatter 

729 

730 def getFormatter(self, entity: Entity, *args: Any, **kwargs: Any) -> Formatter: 

731 """Get a new formatter instance. 

732 

733 Parameters 

734 ---------- 

735 entity : `DatasetRef`, `DatasetType`, `StorageClass`, or `str` 

736 Entity to use to determine the formatter to return. 

737 `StorageClass` will be used as a last resort if `DatasetRef` 

738 or `DatasetType` instance is provided. Supports instrument 

739 override if a `DatasetRef` is provided configured with an 

740 ``instrument`` value for the data ID. 

741 args : `tuple` 

742 Positional arguments to use pass to the object constructor. 

743 kwargs : `dict` 

744 Keyword arguments to pass to object constructor. 

745 

746 Returns 

747 ------- 

748 formatter : `Formatter` 

749 An instance of the registered formatter. 

750 """ 

751 _, formatter = self.getFormatterWithMatch(entity, *args, **kwargs) 

752 return formatter 

753 

754 def registerFormatter(self, type_: Union[LookupKey, str, StorageClass, DatasetType], 

755 formatter: str, *, overwrite: bool = False, 

756 **kwargs: Any) -> None: 

757 """Register a `Formatter`. 

758 

759 Parameters 

760 ---------- 

761 type_ : `LookupKey`, `str`, `StorageClass` or `DatasetType` 

762 Type for which this formatter is to be used. If a `LookupKey` 

763 is not provided, one will be constructed from the supplied string 

764 or by using the ``name`` property of the supplied entity. 

765 formatter : `str` or class of type `Formatter` 

766 Identifies a `Formatter` subclass to use for reading and writing 

767 Datasets of this type. Can be a `Formatter` class. 

768 overwrite : `bool`, optional 

769 If `True` an existing entry will be replaced by the new value. 

770 Default is `False`. 

771 kwargs : `dict` 

772 Keyword arguments to always pass to object constructor when 

773 retrieved. 

774 

775 Raises 

776 ------ 

777 ValueError 

778 Raised if the formatter does not name a valid formatter type and 

779 ``overwrite`` is `False`. 

780 """ 

781 self._mappingFactory.placeInRegistry(type_, formatter, overwrite=overwrite, **kwargs) 

782 

783 

784# Type to use when allowing a Formatter or its class name 

785FormatterParameter = Union[str, Type[Formatter], Formatter]