Coverage for python/lsst/daf/butler/core/formatter.py : 26%

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/>.
22from __future__ import annotations
24__all__ = ("Formatter", "FormatterFactory", "FormatterParameter")
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)
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
55log = logging.getLogger(__name__)
57# Define a new special type for functions that take "entity"
58Entity = Union[DatasetType, DatasetRef, StorageClass, str]
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
65class Formatter(metaclass=ABCMeta):
66 """Interface for reading and writing Datasets with a particular
67 `StorageClass`.
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.
83 Notes
84 -----
85 All Formatter subclasses should share the base class's constructor
86 signature.
87 """
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 """
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."""
100 supportedExtensions: ClassVar[AbstractSet[str]] = frozenset()
101 """Set of all extensions supported by this formatter.
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."""
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
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}")
129 self._writeParameters = writeParameters
130 self._writeRecipes = self.validateWriteRecipes(writeRecipes)
132 def __str__(self) -> str:
133 return f"{self.name()}@{self.fileDescriptor.location.path}"
135 def __repr__(self) -> str:
136 return f"{self.name()}({self.fileDescriptor!r})"
138 @property
139 def fileDescriptor(self) -> FileDescriptor:
140 """FileDescriptor associated with this formatter
141 (`FileDescriptor`, read-only)"""
142 return self._fileDescriptor
144 @property
145 def dataId(self) -> DataCoordinate:
146 """DataId associated with this formatter (`DataCoordinate`)"""
147 return self._dataId
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 {}
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 {}
163 @classmethod
164 def validateWriteRecipes(cls, recipes: Optional[Mapping[str, Any]]) -> Optional[Mapping[str, Any]]:
165 """Validate supplied recipes for this formatter.
167 The recipes are supplemented with default values where appropriate.
169 Parameters
170 ----------
171 recipes : `dict`
172 Recipes to validate.
174 Returns
175 -------
176 validated : `dict`
177 Validated recipes.
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
189 @classmethod
190 def name(cls) -> str:
191 """Returns the fully qualified name of the formatter.
193 Returns
194 -------
195 name : `str`
196 Fully-qualified name of formatter class.
197 """
198 return getFullTypeName(cls)
200 @abstractmethod
201 def read(self, component: Optional[str] = None) -> Any:
202 """Read a Dataset.
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.
211 Returns
212 -------
213 inMemoryDataset : `object`
214 The requested Dataset.
215 """
216 raise NotImplementedError("Type does not support reading")
218 @abstractmethod
219 def write(self, inMemoryDataset: Any) -> None:
220 """Write a Dataset.
222 Parameters
223 ----------
224 inMemoryDataset : `object`
225 The Dataset to store.
226 """
227 raise NotImplementedError("Type does not support writing")
229 @classmethod
230 def can_read_bytes(cls) -> bool:
231 """Indicate if this formatter can format from bytes.
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
250 def fromBytes(self, serializedDataset: bytes,
251 component: Optional[str] = None) -> object:
252 """Reads serialized data into a Dataset or its component.
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.
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.")
271 def toBytes(self, inMemoryDataset: Any) -> bytes:
272 """Serialize the Dataset to bytes based on formatter.
274 Parameters
275 ----------
276 inMemoryDataset : `object`
277 The Python object to serialize.
279 Returns
280 -------
281 serializedDataset : `bytes`
282 Bytes representing the serialized dataset.
283 """
284 raise NotImplementedError("Type does not support writing to bytes.")
286 @contextlib.contextmanager
287 def _updateLocation(self, location: Optional[Location]) -> Iterator[Location]:
288 """Temporarily replace the location associated with this formatter.
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.
299 Yields
300 ------
301 old : `Location`
302 The old location that will be restored.
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
320 def makeUpdatedLocation(self, location: Location) -> Location:
321 """Return a new `Location` instance updated with this formatter's
322 extension.
324 Parameters
325 ----------
326 location : `Location`
327 The location to update.
329 Returns
330 -------
331 updated : `Location`
332 A new `Location` with a new file extension applied.
334 Raises
335 ------
336 NotImplementedError
337 Raised if there is no ``extension`` attribute associated with
338 this formatter.
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
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.
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 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)
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(f"Extension '{location.getExtension()}' on '{location}' "
410 f"is not supported by Formatter '{cls.__name__}' (supports: {supported})")
412 def predictPath(self) -> str:
413 """Return the path that would be returned by write, without actually
414 writing.
416 Uses the `FileDescriptor` associated with the instance.
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
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.
431 Any unsupported parameters are assumed to be usable by associated
432 assemblers.
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`.
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 """
449 if parameters is None:
450 parameters = self.fileDescriptor.parameters
452 if parameters is None:
453 return {}, {}
455 if self.unsupportedParameters is None:
456 # Support none of the parameters
457 return {}, parameters.copy()
459 # Start by assuming all are supported
460 supported = parameters.copy()
461 unsupported = {}
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)
468 return supported, unsupported
471class FormatterFactory:
472 """Factory for `Formatter` instances.
473 """
475 defaultKey = LookupKey("default")
476 """Configuration key associated with default write parameter settings."""
478 writeRecipesKey = LookupKey("write_recipes")
479 """Configuration key associated with write recipes."""
481 def __init__(self) -> None:
482 self._mappingFactory = MappingFactory(Formatter)
484 def __contains__(self, key: Union[LookupKey, str]) -> bool:
485 """Indicates whether the supplied key is present in the factory.
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.
493 Returns
494 -------
495 in : `bool`
496 `True` if the supplied key is present in the factory.
497 """
498 return key in self._mappingFactory
500 def registerFormatters(self, config: Config, *, universe: DimensionUniverse) -> None:
501 """Bulk register formatters from a config.
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.
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.
520 The config is parsed using the function
521 `~lsst.daf.butler.configSubset.processLookupConfigs`.
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:
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.
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.
539 .. code-block:: yaml
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
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``.
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.
567 .. code-block:: yaml
569 formatters:
570 write_recipes:
571 lsst.obs.base.formatters.fitsExposure.FixExposureFormatter:
572 lossless:
573 ...
574 noCompression:
575 ...
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"}
587 contents = processLookupConfigs(config, allow_hierarchy=True, universe=universe)
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)})")
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}'")
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
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}'")
626 # Apply any default parameters for this formatter
627 writeParameters = copy.deepcopy(defaultParameters.get(formatter, {}))
628 writeParameters.update(specificWriteParameters)
630 kwargs: Dict[str, Any] = {}
631 if writeParameters:
632 kwargs["writeParameters"] = writeParameters
634 if formatter in writeRecipes:
635 kwargs["writeRecipes"] = writeRecipes[formatter]
637 self.registerFormatter(key, formatter, **kwargs)
639 def getLookupKeys(self) -> Set[LookupKey]:
640 """Retrieve the look up keys for all the registry entries.
642 Returns
643 -------
644 keys : `set` of `LookupKey`
645 The keys available for matching in the registry.
646 """
647 return self._mappingFactory.getLookupKeys()
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.
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.
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)
677 return matchKey, formatter, formatter_kwargs
679 def getFormatterClass(self, entity: Entity) -> Type:
680 """Get the matching formatter class.
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.
691 Returns
692 -------
693 formatter : `type`
694 The class of the registered formatter.
695 """
696 _, formatter, _ = self.getFormatterClassWithMatch(entity)
697 return formatter
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.
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.
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)
728 return matchKey, formatter
730 def getFormatter(self, entity: Entity, *args: Any, **kwargs: Any) -> Formatter:
731 """Get a new formatter instance.
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.
746 Returns
747 -------
748 formatter : `Formatter`
749 An instance of the registered formatter.
750 """
751 _, formatter = self.getFormatterWithMatch(entity, *args, **kwargs)
752 return formatter
754 def registerFormatter(self, type_: Union[LookupKey, str, StorageClass, DatasetType],
755 formatter: str, *, overwrite: bool = False,
756 **kwargs: Any) -> None:
757 """Register a `Formatter`.
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.
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)
784# Type to use when allowing a Formatter or its class name
785FormatterParameter = Union[str, Type[Formatter], Formatter]