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``.