Custom Types

formatparse allows you to define custom type converters using the with_pattern() decorator. This enables parsing of custom formats and types.

Basic Custom Types

Use the with_pattern() decorator to create a custom type converter:

>>> from formatparse import parse, with_pattern
>>> @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'>

The decorator adds a pattern attribute to your function, which formatparse uses to match the text before calling your converter function.

Compile-time extra_types

Custom-type regex fragments are fixed when the pattern is compiled. Pass extra_types to compile() (or use parse() / search() / findall(), which compile through the shared cache with your mapping) so fields like {:Number} use your @with_pattern regex. If you compile() without extra_types and only supply them on FormatParser.parse(), formatparse still merges converters at call time and re-resolves the cached parser so matching stays consistent with top-level parse() (strict regex, None on mismatch—not a loose \S+ capture followed by a converter error).

Regex Patterns

The pattern parameter is a regular expression that defines what text should match:

>>> @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'

Regex Groups

If your regex pattern contains capturing groups (parentheses), specify the number of groups using the regex_group_count parameter:

>>> @with_pattern(r'(\d+)-(\d+)', regex_group_count=2)
... def parse_range(text):
...     # text will contain the full match, groups are available separately
...     return tuple(map(int, text.split('-')))
>>> result = parse("Range: {:Range}", "Range: 10-20", {"Range": parse_range})
>>> result.fixed[0]
(10, 20)

Integration with parse/search/findall

Custom types work with all parsing functions:

>>> @with_pattern(r'\d+\.\d+')
... def parse_version(text):
...     return tuple(map(int, text.split('.')))
>>> result = parse("Version: {:Version}", "Version: 1.2", extra_types={"Version": parse_version})
>>> result.fixed[0]
(1, 2)

>>> from formatparse import search
>>> result = search("v{:Version}", "Current version v2.5 installed", extra_types={"Version": parse_version})
>>> result.fixed[0]
(2, 5)

Note: search() finds the first match in the string.

>>> from formatparse import findall
>>> results = findall("v{:Version}", "v1.0 v2.0 v3.0", {"Version": parse_version})
>>> len(results)
3
>>> results[0].fixed[0]
(1, 0)

Advanced Examples

Parsing IP Addresses

>>> @with_pattern(r'\d+\.\d+\.\d+\.\d+')
... def parse_ip(text):
...     return tuple(map(int, text.split('.')))
>>> result = parse("IP: {:IP}", "IP: 192.168.1.1", {"IP": parse_ip})
>>> result.fixed[0]
(192, 168, 1, 1)

Parsing Enumerations

>>> STATUS_MAP = {'active': True, 'inactive': False, 'pending': None}
>>> @with_pattern(r'active|inactive|pending')
... def parse_status(text):
...     return STATUS_MAP.get(text.lower())
>>> result = parse("Status: {:Status}", "Status: active", {"Status": parse_status})
>>> result.fixed[0]
True

Complex Parsing

You can combine multiple custom types in a single pattern:

>>> @with_pattern(r'\d+')
... def parse_id(text):
...     return int(text)
>>> @with_pattern(r'[A-Z]+')
... def parse_category(text):
...     return text
>>> result = parse("Item {:ID} in category {:Cat}", "Item 42 in category TOOLS",
...                extra_types={"ID": parse_id, "Cat": parse_category})
>>> result.fixed[0]
42
>>> result.fixed[1]
'TOOLS'

Note: Positional fields (without names) are stored in ``fixed``, not ``named``.