Coverage for python/lsst/daf/butler/_formatter.py: 32%

193 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-26 02:48 -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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ("Formatter", "FormatterFactory", "FormatterParameter") 

31 

32import contextlib 

33import copy 

34import logging 

35from abc import ABCMeta, abstractmethod 

36from collections.abc import Iterator, Mapping, Set 

37from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias 

38 

39from lsst.utils.introspection import get_full_type_name 

40 

41from ._config import Config 

42from ._config_support import LookupKey, processLookupConfigs 

43from ._file_descriptor import FileDescriptor 

44from ._location import Location 

45from .dimensions import DimensionUniverse 

46from .mapping_factory import MappingFactory 

47 

48log = logging.getLogger(__name__) 

49 

50if TYPE_CHECKING: 

51 from ._dataset_ref import DatasetRef 

52 from ._dataset_type import DatasetType 

53 from ._storage_class import StorageClass 

54 from .dimensions import DataCoordinate 

55 

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

57 Entity: TypeAlias = DatasetType | DatasetRef | StorageClass | str 

58 

59 

60class Formatter(metaclass=ABCMeta): 

61 """Interface for reading and writing Datasets. 

62 

63 The formatters are associated with a particular `StorageClass`. 

64 

65 Parameters 

66 ---------- 

67 fileDescriptor : `FileDescriptor`, optional 

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

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

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

71 dataId : `DataCoordinate` 

72 Data ID associated with this formatter. 

73 writeParameters : `dict`, optional 

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

75 the dataset is serialized. 

76 writeRecipes : `dict`, optional 

77 Detailed write Recipes indexed by recipe name. 

78 

79 Notes 

80 ----- 

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

82 signature. 

83 """ 

84 

85 unsupportedParameters: ClassVar[Set[str] | None] = frozenset() 

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

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

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

89 """ 

90 

91 supportedWriteParameters: ClassVar[Set[str] | None] = None 

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

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

94 supported.""" 

95 

96 supportedExtensions: ClassVar[Set[str]] = frozenset() 

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

98 

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

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

101 the list of supported extensions.""" 

102 

103 def __init__( 

104 self, 

105 fileDescriptor: FileDescriptor, 

106 dataId: DataCoordinate, 

107 writeParameters: dict[str, Any] | None = None, 

108 writeRecipes: dict[str, Any] | None = None, 

109 ): 

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( 

120 f"This formatter does not accept any write parameters. Got: {', '.join(writeParameters)}" 

121 ) 

122 else: 

123 given = set(writeParameters) 

124 unknown = given - self.supportedWriteParameters 

125 if unknown: 

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

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

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

129 

130 self._writeParameters = writeParameters 

131 self._writeRecipes = self.validateWriteRecipes(writeRecipes) 

132 

133 def __str__(self) -> str: 

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

135 

136 def __repr__(self) -> str: 

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

138 

139 @property 

140 def fileDescriptor(self) -> FileDescriptor: 

141 """File descriptor associated with this formatter (`FileDescriptor`). 

142 

143 Read-only property. 

144 """ 

145 return self._fileDescriptor 

146 

147 @property 

148 def dataId(self) -> DataCoordinate: 

149 """Return Data ID associated with this formatter (`DataCoordinate`).""" 

150 return self._dataId 

151 

152 @property 

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

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

155 if self._writeParameters is not None: 

156 return self._writeParameters 

157 return {} 

158 

159 @property 

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

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

162 if self._writeRecipes is not None: 

163 return self._writeRecipes 

164 return {} 

165 

166 @classmethod 

167 def validateWriteRecipes(cls, recipes: Mapping[str, Any] | None) -> Mapping[str, Any] | None: 

168 """Validate supplied recipes for this formatter. 

169 

170 The recipes are supplemented with default values where appropriate. 

171 

172 Parameters 

173 ---------- 

174 recipes : `dict` 

175 Recipes to validate. 

176 

177 Returns 

178 ------- 

179 validated : `dict` 

180 Validated recipes. 

181 

182 Raises 

183 ------ 

184 RuntimeError 

185 Raised if validation fails. The default implementation raises 

186 if any recipes are given. 

187 """ 

188 if recipes: 

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

190 return recipes 

191 

192 @classmethod 

193 def name(cls) -> str: 

194 """Return the fully qualified name of the formatter. 

195 

196 Returns 

197 ------- 

198 name : `str` 

199 Fully-qualified name of formatter class. 

200 """ 

201 return get_full_type_name(cls) 

202 

203 @abstractmethod 

204 def read(self, component: str | None = None) -> Any: 

205 """Read a Dataset. 

206 

207 Parameters 

208 ---------- 

209 component : `str`, optional 

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

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

212 file. 

213 

214 Returns 

215 ------- 

216 inMemoryDataset : `object` 

217 The requested Dataset. 

218 """ 

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

220 

221 @abstractmethod 

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

223 """Write a Dataset. 

224 

225 Parameters 

226 ---------- 

227 inMemoryDataset : `object` 

228 The Dataset to store. 

229 """ 

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

231 

232 @classmethod 

233 def can_read_bytes(cls) -> bool: 

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

235 

236 Returns 

237 ------- 

238 can : `bool` 

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

240 """ 

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

242 # and see what happens 

243 try: 

244 # We know the arguments are incompatible 

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

246 except NotImplementedError: 

247 return False 

248 except Exception: 

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

250 pass 

251 return True 

252 

253 def fromBytes(self, serializedDataset: bytes, component: str | None = None) -> object: 

254 """Read serialized data into a Dataset or its component. 

255 

256 Parameters 

257 ---------- 

258 serializedDataset : `bytes` 

259 Bytes object to unserialize. 

260 component : `str`, optional 

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

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

263 file. 

264 

265 Returns 

266 ------- 

267 inMemoryDataset : `object` 

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

269 is controlled by the specific formatter. 

270 """ 

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

272 

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

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

275 

276 Parameters 

277 ---------- 

278 inMemoryDataset : `object` 

279 The Python object to serialize. 

280 

281 Returns 

282 ------- 

283 serializedDataset : `bytes` 

284 Bytes representing the serialized dataset. 

285 """ 

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

287 

288 @contextlib.contextmanager 

289 def _updateLocation(self, location: Location | None) -> Iterator[Location]: 

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

291 

292 Parameters 

293 ---------- 

294 location : `Location` 

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

296 formatter will not change but it will still return 

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

298 path where the location may not need to be updated 

299 but the with block is still convenient. 

300 

301 Yields 

302 ------ 

303 old : `Location` 

304 The old location that will be restored. 

305 

306 Notes 

307 ----- 

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

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

310 manager to restore the location when the temporary is no 

311 longer required. 

312 """ 

313 old = self._fileDescriptor.location 

314 try: 

315 if location is not None: 

316 self._fileDescriptor.location = location 

317 yield old 

318 finally: 

319 if location is not None: 

320 self._fileDescriptor.location = old 

321 

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

323 """Return a new `Location` updated with this formatter's extension. 

324 

325 Parameters 

326 ---------- 

327 location : `Location` 

328 The location to update. 

329 

330 Returns 

331 ------- 

332 updated : `Location` 

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

334 

335 Raises 

336 ------ 

337 NotImplementedError 

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

339 this formatter. 

340 

341 Notes 

342 ----- 

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

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

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

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

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

348 defined in the base class. 

349 """ 

350 location = location.clone() 

351 try: 

352 # We are deliberately allowing extension to be undefined by 

353 # default in the base class and mypy complains. 

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

355 except AttributeError: 

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

357 return location 

358 

359 @classmethod 

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

361 """Check the extension of the provided location for compatibility. 

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 property. 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( 

410 f"Extension '{location.getExtension()}' on '{location}' " 

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

412 ) 

413 

414 def predictPath(self) -> str: 

415 """Return the path that would be returned by write. 

416 

417 Does not write any data file. 

418 

419 Uses the `FileDescriptor` associated with the instance. 

420 

421 Returns 

422 ------- 

423 path : `str` 

424 Path within datastore that would be associated with the location 

425 stored in this `Formatter`. 

426 """ 

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

428 return updated.pathInStore.path 

429 

430 def segregateParameters(self, parameters: dict[str, Any] | None = None) -> tuple[dict, dict]: 

431 """Segregate the supplied parameters. 

432 

433 This splits the parameters into those understood by the 

434 formatter and those not understood by the formatter. 

435 

436 Any unsupported parameters are assumed to be usable by associated 

437 assemblers. 

438 

439 Parameters 

440 ---------- 

441 parameters : `dict`, optional 

442 Parameters with values that have been supplied by the caller 

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

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

445 

446 Returns 

447 ------- 

448 supported : `dict` 

449 Those parameters supported by this formatter. 

450 unsupported : `dict` 

451 Those parameters not supported by this formatter. 

452 """ 

453 if parameters is None: 

454 parameters = self.fileDescriptor.parameters 

455 

456 if parameters is None: 

457 return {}, {} 

458 

459 if self.unsupportedParameters is None: 

460 # Support none of the parameters 

461 return {}, parameters.copy() 

462 

463 # Start by assuming all are supported 

464 supported = parameters.copy() 

465 unsupported = {} 

466 

467 # And remove any we know are not supported 

468 for p in set(supported): 

469 if p in self.unsupportedParameters: 

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

471 

472 return supported, unsupported 

473 

474 

475class FormatterFactory: 

476 """Factory for `Formatter` instances.""" 

477 

478 defaultKey = LookupKey("default") 

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

480 

481 writeRecipesKey = LookupKey("write_recipes") 

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

483 

484 def __init__(self) -> None: 

485 self._mappingFactory = MappingFactory(Formatter) 

486 

487 def __contains__(self, key: LookupKey | str) -> bool: 

488 """Indicate whether the supplied key is present in the factory. 

489 

490 Parameters 

491 ---------- 

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

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

494 formatter is present. 

495 

496 Returns 

497 ------- 

498 in : `bool` 

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

500 """ 

501 return key in self._mappingFactory 

502 

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

504 """Bulk register formatters from a config. 

505 

506 Parameters 

507 ---------- 

508 config : `Config` 

509 ``formatters`` section of a configuration. 

510 universe : `DimensionUniverse`, optional 

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

512 in lookup keys. 

513 

514 Notes 

515 ----- 

516 The configuration can include one level of hierarchy where an 

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

518 template specifications. This is represented in YAML using a 

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

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

521 name in the data ID. 

522 

523 The config is parsed using the function 

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

525 

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

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

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

529 the dict case the following keys are supported: 

530 

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

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

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

534 These parameters are validated at instance creation and not at 

535 configuration. 

536 

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

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

539 default write parameters that should be used whenever an instance 

540 of that class is constructed. 

541 

542 .. code-block:: yaml 

543 

544 formatters: 

545 default: 

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

547 max: 10 

548 min: 2 

549 comment: Default comment 

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

551 coadd: 

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

553 parameters: 

554 max: 5 

555 

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

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

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

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

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

561 ``max``. 

562 

563 Formatter configuration can also include a special section describing 

564 collections of write parameters that can be accessed through a 

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

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

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

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

569 

570 .. code-block:: yaml 

571 

572 formatters: 

573 write_recipes: 

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

575 lossless: 

576 ... 

577 noCompression: 

578 ... 

579 

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

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

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

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

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

585 advance. See the specific formatter documentation for details on 

586 acceptable recipe options. 

587 """ 

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

589 

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

591 

592 # Extract any default parameter settings 

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

594 if not isinstance(defaultParameters, Mapping): 

595 raise RuntimeError( 

596 "Default formatter parameters in config can not be a single string" 

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

598 ) 

599 

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

601 # Formatter class name. 

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

603 if isinstance(writeRecipes, str): 

604 raise RuntimeError( 

605 f"The formatters.{self.writeRecipesKey} section must refer to a dict not '{writeRecipes}'" 

606 ) 

607 

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

609 # default is handled in a special way 

610 if key == self.defaultKey: 

611 continue 

612 if key == self.writeRecipesKey: 

613 continue 

614 

615 # Can be a str or a dict. 

616 specificWriteParameters = {} 

617 if isinstance(f, str): 

618 formatter = f 

619 elif isinstance(f, Mapping): 

620 all_keys = set(f) 

621 unexpected_keys = all_keys - allowed_keys 

622 if unexpected_keys: 

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

624 if "formatter" not in f: 

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

626 formatter = f["formatter"] 

627 if "parameters" in f: 

628 specificWriteParameters = f["parameters"] 

629 else: 

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

631 

632 # Apply any default parameters for this formatter 

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

634 writeParameters.update(specificWriteParameters) 

635 

636 kwargs: dict[str, Any] = {} 

637 if writeParameters: 

638 kwargs["writeParameters"] = writeParameters 

639 

640 if formatter in writeRecipes: 

641 kwargs["writeRecipes"] = writeRecipes[formatter] 

642 

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

644 

645 def getLookupKeys(self) -> set[LookupKey]: 

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

647 

648 Returns 

649 ------- 

650 keys : `set` of `LookupKey` 

651 The keys available for matching in the registry. 

652 """ 

653 return self._mappingFactory.getLookupKeys() 

654 

655 def getFormatterClassWithMatch(self, entity: Entity) -> tuple[LookupKey, type[Formatter], dict[str, Any]]: 

656 """Get the matching formatter class along with the registry key. 

657 

658 Parameters 

659 ---------- 

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

661 Entity to use to determine the formatter to return. 

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

663 or `DatasetType` instance is provided. Supports instrument 

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

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

666 

667 Returns 

668 ------- 

669 matchKey : `LookupKey` 

670 The key that resulted in the successful match. 

671 formatter : `type` 

672 The class of the registered formatter. 

673 formatter_kwargs : `dict` 

674 Keyword arguments that are associated with this formatter entry. 

675 """ 

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

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

678 log.debug( 

679 "Retrieved formatter %s from key '%s' for entity '%s'", 

680 get_full_type_name(formatter), 

681 matchKey, 

682 entity, 

683 ) 

684 

685 return matchKey, formatter, formatter_kwargs 

686 

687 def getFormatterClass(self, entity: Entity) -> type: 

688 """Get the matching formatter class. 

689 

690 Parameters 

691 ---------- 

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

693 Entity to use to determine the formatter to return. 

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

695 or `DatasetType` instance is provided. Supports instrument 

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

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

698 

699 Returns 

700 ------- 

701 formatter : `type` 

702 The class of the registered formatter. 

703 """ 

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

705 return formatter 

706 

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

708 """Get a new formatter instance along with the matching registry key. 

709 

710 Parameters 

711 ---------- 

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

713 Entity to use to determine the formatter to return. 

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

715 or `DatasetType` instance is provided. Supports instrument 

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

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

718 *args : `tuple` 

719 Positional arguments to use pass to the object constructor. 

720 **kwargs 

721 Keyword arguments to pass to object constructor. 

722 

723 Returns 

724 ------- 

725 matchKey : `LookupKey` 

726 The key that resulted in the successful match. 

727 formatter : `Formatter` 

728 An instance of the registered formatter. 

729 """ 

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

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

732 log.debug( 

733 "Retrieved formatter %s from key '%s' for entity '%s'", 

734 get_full_type_name(formatter), 

735 matchKey, 

736 entity, 

737 ) 

738 

739 return matchKey, formatter 

740 

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

742 """Get a new formatter instance. 

743 

744 Parameters 

745 ---------- 

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

747 Entity to use to determine the formatter to return. 

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

749 or `DatasetType` instance is provided. Supports instrument 

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

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

752 *args : `tuple` 

753 Positional arguments to use pass to the object constructor. 

754 **kwargs 

755 Keyword arguments to pass to object constructor. 

756 

757 Returns 

758 ------- 

759 formatter : `Formatter` 

760 An instance of the registered formatter. 

761 """ 

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

763 return formatter 

764 

765 def registerFormatter( 

766 self, 

767 type_: LookupKey | str | StorageClass | DatasetType, 

768 formatter: str, 

769 *, 

770 overwrite: bool = False, 

771 **kwargs: Any, 

772 ) -> None: 

773 """Register a `Formatter`. 

774 

775 Parameters 

776 ---------- 

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

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

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

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

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

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

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

784 overwrite : `bool`, optional 

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

786 Default is `False`. 

787 **kwargs 

788 Keyword arguments to always pass to object constructor when 

789 retrieved. 

790 

791 Raises 

792 ------ 

793 ValueError 

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

795 ``overwrite`` is `False`. 

796 """ 

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

798 

799 

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

801FormatterParameter = str | type[Formatter] | Formatter