Coverage for python/lsst/daf/butler/_formatter.py: 32%
193 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-02 10:24 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-02 10:24 +0000
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/>.
28from __future__ import annotations
30__all__ = ("Formatter", "FormatterFactory", "FormatterParameter")
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
39from lsst.utils.introspection import get_full_type_name
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
48log = logging.getLogger(__name__)
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
56 # Define a new special type for functions that take "entity"
57 Entity: TypeAlias = DatasetType | DatasetRef | StorageClass | str
60class Formatter(metaclass=ABCMeta):
61 """Interface for reading and writing Datasets.
63 The formatters are associated with a particular `StorageClass`.
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.
79 Notes
80 -----
81 All Formatter subclasses should share the base class's constructor
82 signature.
83 """
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 """
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."""
96 supportedExtensions: ClassVar[Set[str]] = frozenset()
97 """Set of all extensions supported by this formatter.
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."""
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
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}")
130 self._writeParameters = writeParameters
131 self._writeRecipes = self.validateWriteRecipes(writeRecipes)
133 def __str__(self) -> str:
134 return f"{self.name()}@{self.fileDescriptor.location.path}"
136 def __repr__(self) -> str:
137 return f"{self.name()}({self.fileDescriptor!r})"
139 @property
140 def fileDescriptor(self) -> FileDescriptor:
141 """File descriptor associated with this formatter (`FileDescriptor`).
143 Read-only property.
144 """
145 return self._fileDescriptor
147 @property
148 def dataId(self) -> DataCoordinate:
149 """Return Data ID associated with this formatter (`DataCoordinate`)."""
150 return self._dataId
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 {}
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 {}
166 @classmethod
167 def validateWriteRecipes(cls, recipes: Mapping[str, Any] | None) -> Mapping[str, Any] | None:
168 """Validate supplied recipes for this formatter.
170 The recipes are supplemented with default values where appropriate.
172 Parameters
173 ----------
174 recipes : `dict`
175 Recipes to validate.
177 Returns
178 -------
179 validated : `dict`
180 Validated recipes.
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
192 @classmethod
193 def name(cls) -> str:
194 """Return the fully qualified name of the formatter.
196 Returns
197 -------
198 name : `str`
199 Fully-qualified name of formatter class.
200 """
201 return get_full_type_name(cls)
203 @abstractmethod
204 def read(self, component: str | None = None) -> Any:
205 """Read a Dataset.
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.
214 Returns
215 -------
216 inMemoryDataset : `object`
217 The requested Dataset.
218 """
219 raise NotImplementedError("Type does not support reading")
221 @abstractmethod
222 def write(self, inMemoryDataset: Any) -> None:
223 """Write a Dataset.
225 Parameters
226 ----------
227 inMemoryDataset : `object`
228 The Dataset to store.
229 """
230 raise NotImplementedError("Type does not support writing")
232 @classmethod
233 def can_read_bytes(cls) -> bool:
234 """Indicate if this formatter can format from bytes.
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
253 def fromBytes(self, serializedDataset: bytes, component: str | None = None) -> object:
254 """Read serialized data into a Dataset or its component.
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.
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.")
273 def toBytes(self, inMemoryDataset: Any) -> bytes:
274 """Serialize the Dataset to bytes based on formatter.
276 Parameters
277 ----------
278 inMemoryDataset : `object`
279 The Python object to serialize.
281 Returns
282 -------
283 serializedDataset : `bytes`
284 Bytes representing the serialized dataset.
285 """
286 raise NotImplementedError("Type does not support writing to bytes.")
288 @contextlib.contextmanager
289 def _updateLocation(self, location: Location | None) -> Iterator[Location]:
290 """Temporarily replace the location associated with this formatter.
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.
301 Yields
302 ------
303 old : `Location`
304 The old location that will be restored.
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
322 def makeUpdatedLocation(self, location: Location) -> Location:
323 """Return a new `Location` updated with this formatter's extension.
325 Parameters
326 ----------
327 location : `Location`
328 The location to update.
330 Returns
331 -------
332 updated : `Location`
333 A new `Location` with a new file extension applied.
335 Raises
336 ------
337 NotImplementedError
338 Raised if there is no ``extension`` attribute associated with
339 this formatter.
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
359 @classmethod
360 def validateExtension(cls, location: Location) -> None:
361 """Check the extension of the provided location for compatibility.
363 Parameters
364 ----------
365 location : `Location`
366 Location from which to extract a file extension.
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.
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)
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
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)
399 # Get the file name from the uri
400 file = location.uri.basename()
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
409 raise ValueError(
410 f"Extension '{location.getExtension()}' on '{location}' "
411 f"is not supported by Formatter '{cls.__name__}' (supports: {supported})"
412 )
414 def predictPath(self) -> str:
415 """Return the path that would be returned by write.
417 Does not write any data file.
419 Uses the `FileDescriptor` associated with the instance.
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
430 def segregateParameters(self, parameters: dict[str, Any] | None = None) -> tuple[dict, dict]:
431 """Segregate the supplied parameters.
433 This splits the parameters into those understood by the
434 formatter and those not understood by the formatter.
436 Any unsupported parameters are assumed to be usable by associated
437 assemblers.
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`.
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
456 if parameters is None:
457 return {}, {}
459 if self.unsupportedParameters is None:
460 # Support none of the parameters
461 return {}, parameters.copy()
463 # Start by assuming all are supported
464 supported = parameters.copy()
465 unsupported = {}
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)
472 return supported, unsupported
475class FormatterFactory:
476 """Factory for `Formatter` instances."""
478 defaultKey = LookupKey("default")
479 """Configuration key associated with default write parameter settings."""
481 writeRecipesKey = LookupKey("write_recipes")
482 """Configuration key associated with write recipes."""
484 def __init__(self) -> None:
485 self._mappingFactory = MappingFactory(Formatter)
487 def __contains__(self, key: LookupKey | str) -> bool:
488 """Indicate whether the supplied key is present in the factory.
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.
496 Returns
497 -------
498 in : `bool`
499 `True` if the supplied key is present in the factory.
500 """
501 return key in self._mappingFactory
503 def registerFormatters(self, config: Config, *, universe: DimensionUniverse) -> None:
504 """Bulk register formatters from a config.
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.
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.
523 The config is parsed using the function
524 `~lsst.daf.butler.configSubset.processLookupConfigs`.
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:
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.
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.
542 .. code-block:: yaml
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
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``.
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.
570 .. code-block:: yaml
572 formatters:
573 write_recipes:
574 lsst.obs.base.formatters.fitsExposure.FixExposureFormatter:
575 lossless:
576 ...
577 noCompression:
578 ...
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"}
590 contents = processLookupConfigs(config, allow_hierarchy=True, universe=universe)
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 )
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 )
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
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}'")
632 # Apply any default parameters for this formatter
633 writeParameters = copy.deepcopy(defaultParameters.get(formatter, {}))
634 writeParameters.update(specificWriteParameters)
636 kwargs: dict[str, Any] = {}
637 if writeParameters:
638 kwargs["writeParameters"] = writeParameters
640 if formatter in writeRecipes:
641 kwargs["writeRecipes"] = writeRecipes[formatter]
643 self.registerFormatter(key, formatter, **kwargs)
645 def getLookupKeys(self) -> set[LookupKey]:
646 """Retrieve the look up keys for all the registry entries.
648 Returns
649 -------
650 keys : `set` of `LookupKey`
651 The keys available for matching in the registry.
652 """
653 return self._mappingFactory.getLookupKeys()
655 def getFormatterClassWithMatch(self, entity: Entity) -> tuple[LookupKey, type[Formatter], dict[str, Any]]:
656 """Get the matching formatter class along with the registry key.
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.
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 )
685 return matchKey, formatter, formatter_kwargs
687 def getFormatterClass(self, entity: Entity) -> type:
688 """Get the matching formatter class.
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.
699 Returns
700 -------
701 formatter : `type`
702 The class of the registered formatter.
703 """
704 _, formatter, _ = self.getFormatterClassWithMatch(entity)
705 return formatter
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.
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.
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 )
739 return matchKey, formatter
741 def getFormatter(self, entity: Entity, *args: Any, **kwargs: Any) -> Formatter:
742 """Get a new formatter instance.
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.
757 Returns
758 -------
759 formatter : `Formatter`
760 An instance of the registered formatter.
761 """
762 _, formatter = self.getFormatterWithMatch(entity, *args, **kwargs)
763 return formatter
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`.
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.
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)
800# Type to use when allowing a Formatter or its class name
801FormatterParameter = str | type[Formatter] | Formatter