Coverage for python/lsst/daf/butler/core/formatter.py: 33%
196 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-05 01:26 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-05 01:26 +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 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")
26import contextlib
27import copy
28import logging
29from abc import ABCMeta, abstractmethod
30from collections.abc import Iterator, Mapping, Set
31from typing import TYPE_CHECKING, Any, ClassVar
33from lsst.utils.introspection import get_full_type_name
35from .config import Config
36from .configSupport import LookupKey, processLookupConfigs
37from .datasets import DatasetRef, DatasetType
38from .dimensions import DimensionUniverse
39from .fileDescriptor import FileDescriptor
40from .location import Location
41from .mappingFactory import MappingFactory
42from .storageClass import StorageClass
44log = logging.getLogger(__name__)
46# Define a new special type for functions that take "entity"
47Entity = DatasetType | DatasetRef | StorageClass | str
50if TYPE_CHECKING:
51 from .dimensions import DataCoordinate
54class Formatter(metaclass=ABCMeta):
55 """Interface for reading and writing Datasets.
57 The formatters are associated with a particular `StorageClass`.
59 Parameters
60 ----------
61 fileDescriptor : `FileDescriptor`, optional
62 Identifies the file to read or write, and the associated storage
63 classes and parameter information. Its value can be `None` if the
64 caller will never call `Formatter.read` or `Formatter.write`.
65 dataId : `DataCoordinate`
66 Data ID associated with this formatter.
67 writeParameters : `dict`, optional
68 Any parameters to be hard-coded into this instance to control how
69 the dataset is serialized.
70 writeRecipes : `dict`, optional
71 Detailed write Recipes indexed by recipe name.
73 Notes
74 -----
75 All Formatter subclasses should share the base class's constructor
76 signature.
77 """
79 unsupportedParameters: ClassVar[Set[str] | None] = frozenset()
80 """Set of read parameters not understood by this `Formatter`. An empty set
81 means all parameters are supported. `None` indicates that no parameters
82 are supported. These param (`frozenset`).
83 """
85 supportedWriteParameters: ClassVar[Set[str] | None] = None
86 """Parameters understood by this formatter that can be used to control
87 how a dataset is serialized. `None` indicates that no parameters are
88 supported."""
90 supportedExtensions: ClassVar[Set[str]] = frozenset()
91 """Set of all extensions supported by this formatter.
93 Only expected to be populated by Formatters that write files. Any extension
94 assigned to the ``extension`` property will be automatically included in
95 the list of supported extensions."""
97 def __init__(
98 self,
99 fileDescriptor: FileDescriptor,
100 dataId: DataCoordinate,
101 writeParameters: dict[str, Any] | None = None,
102 writeRecipes: dict[str, Any] | None = None,
103 ):
104 if not isinstance(fileDescriptor, FileDescriptor):
105 raise TypeError("File descriptor must be a FileDescriptor")
106 assert dataId is not None, "dataId is now required for formatter initialization"
107 self._fileDescriptor = fileDescriptor
108 self._dataId = dataId
110 # Check that the write parameters are allowed
111 if writeParameters:
112 if self.supportedWriteParameters is None:
113 raise ValueError(
114 f"This formatter does not accept any write parameters. Got: {', '.join(writeParameters)}"
115 )
116 else:
117 given = set(writeParameters)
118 unknown = given - self.supportedWriteParameters
119 if unknown:
120 s = "s" if len(unknown) != 1 else ""
121 unknownStr = ", ".join(f"'{u}'" for u in unknown)
122 raise ValueError(f"This formatter does not accept parameter{s} {unknownStr}")
124 self._writeParameters = writeParameters
125 self._writeRecipes = self.validateWriteRecipes(writeRecipes)
127 def __str__(self) -> str:
128 return f"{self.name()}@{self.fileDescriptor.location.path}"
130 def __repr__(self) -> str:
131 return f"{self.name()}({self.fileDescriptor!r})"
133 @property
134 def fileDescriptor(self) -> FileDescriptor:
135 """File descriptor associated with this formatter (`FileDescriptor`).
137 Read-only property.
138 """
139 return self._fileDescriptor
141 @property
142 def dataId(self) -> DataCoordinate:
143 """Return Data ID associated with this formatter (`DataCoordinate`)."""
144 return self._dataId
146 @property
147 def writeParameters(self) -> Mapping[str, Any]:
148 """Parameters to use when writing out datasets."""
149 if self._writeParameters is not None:
150 return self._writeParameters
151 return {}
153 @property
154 def writeRecipes(self) -> Mapping[str, Any]:
155 """Detailed write Recipes indexed by recipe name."""
156 if self._writeRecipes is not None:
157 return self._writeRecipes
158 return {}
160 @classmethod
161 def validateWriteRecipes(cls, recipes: Mapping[str, Any] | None) -> Mapping[str, Any] | None:
162 """Validate supplied recipes for this formatter.
164 The recipes are supplemented with default values where appropriate.
166 Parameters
167 ----------
168 recipes : `dict`
169 Recipes to validate.
171 Returns
172 -------
173 validated : `dict`
174 Validated recipes.
176 Raises
177 ------
178 RuntimeError
179 Raised if validation fails. The default implementation raises
180 if any recipes are given.
181 """
182 if recipes:
183 raise RuntimeError(f"This formatter does not understand these writeRecipes: {recipes}")
184 return recipes
186 @classmethod
187 def name(cls) -> str:
188 """Return the fully qualified name of the formatter.
190 Returns
191 -------
192 name : `str`
193 Fully-qualified name of formatter class.
194 """
195 return get_full_type_name(cls)
197 @abstractmethod
198 def read(self, component: str | None = None) -> Any:
199 """Read a Dataset.
201 Parameters
202 ----------
203 component : `str`, optional
204 Component to read from the file. Only used if the `StorageClass`
205 for reading differed from the `StorageClass` used to write the
206 file.
208 Returns
209 -------
210 inMemoryDataset : `object`
211 The requested Dataset.
212 """
213 raise NotImplementedError("Type does not support reading")
215 @abstractmethod
216 def write(self, inMemoryDataset: Any) -> None:
217 """Write a Dataset.
219 Parameters
220 ----------
221 inMemoryDataset : `object`
222 The Dataset to store.
223 """
224 raise NotImplementedError("Type does not support writing")
226 @classmethod
227 def can_read_bytes(cls) -> bool:
228 """Indicate if this formatter can format from bytes.
230 Returns
231 -------
232 can : `bool`
233 `True` if the `fromBytes` method is implemented.
234 """
235 # We have no property to read so instead try to format from a byte
236 # and see what happens
237 try:
238 # We know the arguments are incompatible
239 cls.fromBytes(cls, b"") # type: ignore
240 except NotImplementedError:
241 return False
242 except Exception:
243 # There will be problems with the bytes we are supplying so ignore
244 pass
245 return True
247 def fromBytes(self, serializedDataset: bytes, component: str | None = None) -> object:
248 """Read serialized data into a Dataset or its component.
250 Parameters
251 ----------
252 serializedDataset : `bytes`
253 Bytes object to unserialize.
254 component : `str`, optional
255 Component to read from the Dataset. Only used if the `StorageClass`
256 for reading differed from the `StorageClass` used to write the
257 file.
259 Returns
260 -------
261 inMemoryDataset : `object`
262 The requested data as a Python object. The type of object
263 is controlled by the specific formatter.
264 """
265 raise NotImplementedError("Type does not support reading from bytes.")
267 def toBytes(self, inMemoryDataset: Any) -> bytes:
268 """Serialize the Dataset to bytes based on formatter.
270 Parameters
271 ----------
272 inMemoryDataset : `object`
273 The Python object to serialize.
275 Returns
276 -------
277 serializedDataset : `bytes`
278 Bytes representing the serialized dataset.
279 """
280 raise NotImplementedError("Type does not support writing to bytes.")
282 @contextlib.contextmanager
283 def _updateLocation(self, location: Location | None) -> Iterator[Location]:
284 """Temporarily replace the location associated with this formatter.
286 Parameters
287 ----------
288 location : `Location`
289 New location to use for this formatter. If `None` the
290 formatter will not change but it will still return
291 the old location. This allows it to be used in a code
292 path where the location may not need to be updated
293 but the with block is still convenient.
295 Yields
296 ------
297 old : `Location`
298 The old location that will be restored.
300 Notes
301 -----
302 This is an internal method that should be used with care.
303 It may change in the future. Should be used as a context
304 manager to restore the location when the temporary is no
305 longer required.
306 """
307 old = self._fileDescriptor.location
308 try:
309 if location is not None:
310 self._fileDescriptor.location = location
311 yield old
312 finally:
313 if location is not None:
314 self._fileDescriptor.location = old
316 def makeUpdatedLocation(self, location: Location) -> Location:
317 """Return a new `Location` updated with this formatter's extension.
319 Parameters
320 ----------
321 location : `Location`
322 The location to update.
324 Returns
325 -------
326 updated : `Location`
327 A new `Location` with a new file extension applied.
329 Raises
330 ------
331 NotImplementedError
332 Raised if there is no ``extension`` attribute associated with
333 this formatter.
335 Notes
336 -----
337 This method is available to all Formatters but might not be
338 implemented by all formatters. It requires that a formatter set
339 an ``extension`` attribute containing the file extension used when
340 writing files. If ``extension`` is `None` the supplied file will
341 not be updated. Not all formatters write files so this is not
342 defined in the base class.
343 """
344 location = copy.deepcopy(location)
345 try:
346 # We are deliberately allowing extension to be undefined by
347 # default in the base class and mypy complains.
348 location.updateExtension(self.extension) # type:ignore
349 except AttributeError:
350 raise NotImplementedError("No file extension registered with this formatter") from None
351 return location
353 @classmethod
354 def validateExtension(cls, location: Location) -> None:
355 """Check the extension of the provided location for compatibility.
357 Parameters
358 ----------
359 location : `Location`
360 Location from which to extract a file extension.
362 Raises
363 ------
364 NotImplementedError
365 Raised if file extensions are a concept not understood by this
366 formatter.
367 ValueError
368 Raised if the formatter does not understand this extension.
370 Notes
371 -----
372 This method is available to all Formatters but might not be
373 implemented by all formatters. It requires that a formatter set
374 an ``extension`` attribute containing the file extension used when
375 writing files. If ``extension`` is `None` only the set of supported
376 extensions will be examined.
377 """
378 supported = set(cls.supportedExtensions)
380 try:
381 # We are deliberately allowing extension to be undefined by
382 # default in the base class and mypy complains.
383 default = cls.extension # type: ignore
384 except AttributeError:
385 raise NotImplementedError("No file extension registered with this formatter") from None
387 # If extension is implemented as an instance property it won't return
388 # a string when called as a class property. Assume that
389 # the supported extensions class property is complete.
390 if default is not None and isinstance(default, str):
391 supported.add(default)
393 # Get the file name from the uri
394 file = location.uri.basename()
396 # Check that this file name ends with one of the supported extensions.
397 # This is less prone to confusion than asking the location for
398 # its extension and then doing a set comparison
399 for ext in supported:
400 if file.endswith(ext):
401 return
403 raise ValueError(
404 f"Extension '{location.getExtension()}' on '{location}' "
405 f"is not supported by Formatter '{cls.__name__}' (supports: {supported})"
406 )
408 def predictPath(self) -> str:
409 """Return the path that would be returned by write.
411 Does not write any data file.
413 Uses the `FileDescriptor` associated with the instance.
415 Returns
416 -------
417 path : `str`
418 Path within datastore that would be associated with the location
419 stored in this `Formatter`.
420 """
421 updated = self.makeUpdatedLocation(self.fileDescriptor.location)
422 return updated.pathInStore.path
424 def segregateParameters(self, parameters: dict[str, Any] | None = None) -> tuple[dict, dict]:
425 """Segregate the supplied parameters.
427 This splits the parameters into those understood by the
428 formatter and those not understood by the formatter.
430 Any unsupported parameters are assumed to be usable by associated
431 assemblers.
433 Parameters
434 ----------
435 parameters : `dict`, optional
436 Parameters with values that have been supplied by the caller
437 and which might be relevant for the formatter. If `None`
438 parameters will be read from the registered `FileDescriptor`.
440 Returns
441 -------
442 supported : `dict`
443 Those parameters supported by this formatter.
444 unsupported : `dict`
445 Those parameters not supported by this formatter.
446 """
447 if parameters is None:
448 parameters = self.fileDescriptor.parameters
450 if parameters is None:
451 return {}, {}
453 if self.unsupportedParameters is None:
454 # Support none of the parameters
455 return {}, parameters.copy()
457 # Start by assuming all are supported
458 supported = parameters.copy()
459 unsupported = {}
461 # And remove any we know are not supported
462 for p in set(supported):
463 if p in self.unsupportedParameters:
464 unsupported[p] = supported.pop(p)
466 return supported, unsupported
469class FormatterFactory:
470 """Factory for `Formatter` instances."""
472 defaultKey = LookupKey("default")
473 """Configuration key associated with default write parameter settings."""
475 writeRecipesKey = LookupKey("write_recipes")
476 """Configuration key associated with write recipes."""
478 def __init__(self) -> None:
479 self._mappingFactory = MappingFactory(Formatter)
481 def __contains__(self, key: LookupKey | str) -> bool:
482 """Indicate whether the supplied key is present in the factory.
484 Parameters
485 ----------
486 key : `LookupKey`, `str` or objects with ``name`` attribute
487 Key to use to lookup in the factory whether a corresponding
488 formatter is present.
490 Returns
491 -------
492 in : `bool`
493 `True` if the supplied key is present in the factory.
494 """
495 return key in self._mappingFactory
497 def registerFormatters(self, config: Config, *, universe: DimensionUniverse) -> None:
498 """Bulk register formatters from a config.
500 Parameters
501 ----------
502 config : `Config`
503 ``formatters`` section of a configuration.
504 universe : `DimensionUniverse`, optional
505 Set of all known dimensions, used to expand and validate any used
506 in lookup keys.
508 Notes
509 -----
510 The configuration can include one level of hierarchy where an
511 instrument-specific section can be defined to override more general
512 template specifications. This is represented in YAML using a
513 key of form ``instrument<name>`` which can then define templates
514 that will be returned if a `DatasetRef` contains a matching instrument
515 name in the data ID.
517 The config is parsed using the function
518 `~lsst.daf.butler.configSubset.processLookupConfigs`.
520 The values for formatter entries can be either a simple string
521 referring to a python type or a dict representing the formatter and
522 parameters to be hard-coded into the formatter constructor. For
523 the dict case the following keys are supported:
525 - formatter: The python type to be used as the formatter class.
526 - parameters: A further dict to be passed directly to the
527 ``writeParameters`` Formatter constructor to seed it.
528 These parameters are validated at instance creation and not at
529 configuration.
531 Additionally, a special ``default`` section can be defined that
532 uses the formatter type (class) name as the keys and specifies
533 default write parameters that should be used whenever an instance
534 of that class is constructed.
536 .. code-block:: yaml
538 formatters:
539 default:
540 lsst.daf.butler.formatters.example.ExampleFormatter:
541 max: 10
542 min: 2
543 comment: Default comment
544 calexp: lsst.daf.butler.formatters.example.ExampleFormatter
545 coadd:
546 formatter: lsst.daf.butler.formatters.example.ExampleFormatter
547 parameters:
548 max: 5
550 Any time an ``ExampleFormatter`` is constructed it will use those
551 parameters. If an explicit entry later in the configuration specifies
552 a different set of parameters, the two will be merged with the later
553 entry taking priority. In the example above ``calexp`` will use
554 the default parameters but ``coadd`` will override the value for
555 ``max``.
557 Formatter configuration can also include a special section describing
558 collections of write parameters that can be accessed through a
559 simple label. This allows common collections of options to be
560 specified in one place in the configuration and reused later.
561 The ``write_recipes`` section is indexed by Formatter class name
562 and each key is the label to associate with the parameters.
564 .. code-block:: yaml
566 formatters:
567 write_recipes:
568 lsst.obs.base.formatters.fitsExposure.FixExposureFormatter:
569 lossless:
570 ...
571 noCompression:
572 ...
574 By convention a formatter that uses write recipes will support a
575 ``recipe`` write parameter that will refer to a recipe name in
576 the ``write_recipes`` component. The `Formatter` will be constructed
577 in the `FormatterFactory` with all the relevant recipes and
578 will not attempt to filter by looking at ``writeParameters`` in
579 advance. See the specific formatter documentation for details on
580 acceptable recipe options.
581 """
582 allowed_keys = {"formatter", "parameters"}
584 contents = processLookupConfigs(config, allow_hierarchy=True, universe=universe)
586 # Extract any default parameter settings
587 defaultParameters = contents.get(self.defaultKey, {})
588 if not isinstance(defaultParameters, Mapping):
589 raise RuntimeError(
590 "Default formatter parameters in config can not be a single string"
591 f" (got: {type(defaultParameters)})"
592 )
594 # Extract any global write recipes -- these are indexed by
595 # Formatter class name.
596 writeRecipes = contents.get(self.writeRecipesKey, {})
597 if isinstance(writeRecipes, str):
598 raise RuntimeError(
599 f"The formatters.{self.writeRecipesKey} section must refer to a dict not '{writeRecipes}'"
600 )
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], dict[str, Any]]:
650 """Get the matching formatter class along with the registry key.
652 Parameters
653 ----------
654 entity : `DatasetRef`, `DatasetType`, `StorageClass`, or `str`
655 Entity to use to determine the formatter to return.
656 `StorageClass` will be used as a last resort if `DatasetRef`
657 or `DatasetType` instance is provided. Supports instrument
658 override if a `DatasetRef` is provided configured with an
659 ``instrument`` value for the data ID.
661 Returns
662 -------
663 matchKey : `LookupKey`
664 The key that resulted in the successful match.
665 formatter : `type`
666 The class of the registered formatter.
667 formatter_kwargs : `dict`
668 Keyword arguments that are associated with this formatter entry.
669 """
670 names = (LookupKey(name=entity),) if isinstance(entity, str) else entity._lookupNames()
671 matchKey, formatter, formatter_kwargs = self._mappingFactory.getClassFromRegistryWithMatch(names)
672 log.debug(
673 "Retrieved formatter %s from key '%s' for entity '%s'",
674 get_full_type_name(formatter),
675 matchKey,
676 entity,
677 )
679 return matchKey, formatter, formatter_kwargs
681 def getFormatterClass(self, entity: Entity) -> type:
682 """Get the matching formatter class.
684 Parameters
685 ----------
686 entity : `DatasetRef`, `DatasetType`, `StorageClass`, or `str`
687 Entity to use to determine the formatter to return.
688 `StorageClass` will be used as a last resort if `DatasetRef`
689 or `DatasetType` instance is provided. Supports instrument
690 override if a `DatasetRef` is provided configured with an
691 ``instrument`` value for the data ID.
693 Returns
694 -------
695 formatter : `type`
696 The class of the registered formatter.
697 """
698 _, formatter, _ = self.getFormatterClassWithMatch(entity)
699 return formatter
701 def getFormatterWithMatch(self, entity: Entity, *args: Any, **kwargs: Any) -> tuple[LookupKey, Formatter]:
702 """Get a new formatter instance along with the matching registry key.
704 Parameters
705 ----------
706 entity : `DatasetRef`, `DatasetType`, `StorageClass`, or `str`
707 Entity to use to determine the formatter to return.
708 `StorageClass` will be used as a last resort if `DatasetRef`
709 or `DatasetType` instance is provided. Supports instrument
710 override if a `DatasetRef` is provided configured with an
711 ``instrument`` value for the data ID.
712 args : `tuple`
713 Positional arguments to use pass to the object constructor.
714 **kwargs
715 Keyword arguments to pass to object constructor.
717 Returns
718 -------
719 matchKey : `LookupKey`
720 The key that resulted in the successful match.
721 formatter : `Formatter`
722 An instance of the registered formatter.
723 """
724 names = (LookupKey(name=entity),) if isinstance(entity, str) else entity._lookupNames()
725 matchKey, formatter = self._mappingFactory.getFromRegistryWithMatch(names, *args, **kwargs)
726 log.debug(
727 "Retrieved formatter %s from key '%s' for entity '%s'",
728 get_full_type_name(formatter),
729 matchKey,
730 entity,
731 )
733 return matchKey, formatter
735 def getFormatter(self, entity: Entity, *args: Any, **kwargs: Any) -> Formatter:
736 """Get a new formatter instance.
738 Parameters
739 ----------
740 entity : `DatasetRef`, `DatasetType`, `StorageClass`, or `str`
741 Entity to use to determine the formatter to return.
742 `StorageClass` will be used as a last resort if `DatasetRef`
743 or `DatasetType` instance is provided. Supports instrument
744 override if a `DatasetRef` is provided configured with an
745 ``instrument`` value for the data ID.
746 args : `tuple`
747 Positional arguments to use pass to the object constructor.
748 **kwargs
749 Keyword arguments to pass to object constructor.
751 Returns
752 -------
753 formatter : `Formatter`
754 An instance of the registered formatter.
755 """
756 _, formatter = self.getFormatterWithMatch(entity, *args, **kwargs)
757 return formatter
759 def registerFormatter(
760 self,
761 type_: LookupKey | str | StorageClass | DatasetType,
762 formatter: str,
763 *,
764 overwrite: bool = False,
765 **kwargs: Any,
766 ) -> None:
767 """Register a `Formatter`.
769 Parameters
770 ----------
771 type_ : `LookupKey`, `str`, `StorageClass` or `DatasetType`
772 Type for which this formatter is to be used. If a `LookupKey`
773 is not provided, one will be constructed from the supplied string
774 or by using the ``name`` property of the supplied entity.
775 formatter : `str` or class of type `Formatter`
776 Identifies a `Formatter` subclass to use for reading and writing
777 Datasets of this type. Can be a `Formatter` class.
778 overwrite : `bool`, optional
779 If `True` an existing entry will be replaced by the new value.
780 Default is `False`.
781 **kwargs
782 Keyword arguments to always pass to object constructor when
783 retrieved.
785 Raises
786 ------
787 ValueError
788 Raised if the formatter does not name a valid formatter type and
789 ``overwrite`` is `False`.
790 """
791 self._mappingFactory.placeInRegistry(type_, formatter, overwrite=overwrite, **kwargs)
794# Type to use when allowing a Formatter or its class name
795FormatterParameter = str | type[Formatter] | Formatter