Source code for formatparse.custom

"""Custom type converters and composition."""

from __future__ import annotations

from typing import Any, Callable, TypeVar, cast

from ._native import FormatParser, ParseResult
from .types import ConverterProtocol


F = TypeVar("F", bound=Callable[[str], Any])


[docs] def with_pattern( pattern: str, regex_group_count: int = 0 ) -> Callable[[F], ConverterProtocol]: """Decorator to create a custom type converter with a regex pattern. This decorator adds a ``pattern`` attribute to the converter function, which is used by the parse functions when matching custom types. :param pattern: The regex pattern to match :type pattern: str :param regex_group_count: Number of regex groups in the pattern (for parentheses) (default: 0) :type regex_group_count: int :returns: Decorator function that adds the pattern attribute :rtype: Callable Example:: >>> @with_pattern(r'\\d+') ... def parse_number(text): ... return int(text) >>> result = parse("Answer: {:Number}", "Answer: 42", {"Number": parse_number}) >>> result.fixed[0] 42 >>> type(result.fixed[0]) <class 'int'> >>> @with_pattern(r'[A-Z]{2,3}') ... def parse_code(text): ... return text.upper() >>> result = parse("Code: {:Code}", "Code: abc", {"Code": parse_code}) >>> result.fixed[0] 'ABC' """ def decorator(func: F) -> ConverterProtocol: setattr(func, "pattern", pattern) setattr(func, "regex_group_count", regex_group_count) return cast(ConverterProtocol, func) return decorator
[docs] class ComposedType: """Wrap a compiled :class:`FormatParser` for use as one ``extra_types`` converter. Instances expose ``pattern`` and ``regex_group_count`` for the parent's regex builder (GitHub issue #7). Prefer constructing via :func:`composed_type`. Pickling a parent :class:`FormatParser` still does not restore ``extra_types``; rebuild composed mappings after unpickling. """ __slots__ = ("_parser", "pattern", "regex_group_count")
[docs] def __init__(self, parser: FormatParser) -> None: self._parser = parser self.pattern = parser.regex_subpattern self.regex_group_count = parser.regex_capturing_group_count
def __call__(self, text: str) -> ParseResult: result = self._parser.parse(text) if result is None: raise ValueError( "Composed sub-parser did not match the captured text; the child " "pattern must accept the substring matched by the parent field." ) return result
[docs] def composed_type(parser: FormatParser) -> ComposedType: """Wrap a compiled parser for embedding in another pattern's ``extra_types``. The parent pattern refers to a custom type name; the value is this wrapper, which delegates parsing of the captured substring to ``parser``. Example:: >>> from formatparse import compile, composed_type >>> ts = compile("{year:d}-{month:02d}-{day:02d}") >>> log = compile( ... "{ts:Timestamp} [{level}] {msg}", ... extra_types={"Timestamp": composed_type(ts)}, ... ) >>> r = log.parse("2024-01-15 [ERROR] oops") >>> r.named["level"] 'ERROR' >>> r.named["msg"] 'oops' >>> inner = r.named["ts"] >>> inner.named["year"], inner.named["month"], inner.named["day"] (2024, 1, 15) :param parser: Child parser produced by :func:`compile`. :returns: Callable with ``pattern`` / ``regex_group_count`` set for composition. .. note:: Pattern ``+``, inheritance, and flattening nested fields into the parent result are not implemented yet (see issue #7). """ return ComposedType(parser)