Usage¶
This page covers building a Rebulk, declaring the three pattern types, tuning pattern
options, and reading back the Match / Matches results.
Building a Rebulk¶
A Rebulk object collects patterns (and rules) through a fluent API: every builder
method returns the same object, so calls can be chained. Call matches(string) to run
the full pipeline (pattern matching, then rules) and get back a Matches sequence.
>>> from rebulk import Rebulk
>>> bulk = Rebulk().string('brown').regex(r'qu\w+').functional(lambda s: (20, 25))
>>> bulk.matches("The quick brown fox jumps over the lazy dog")
[<brown:(10, 15)>, <quick:(4, 9)>, <jumps:(20, 25)>]
Rebulk objects can be nested with Rebulk.rebulk(...) to compose several pattern sets
into one matcher.
String patterns¶
String patterns are based on
str.find, but return every
occurrence in the string. ignore_case can be enabled to match regardless of case.
>>> Rebulk().string('la').matches("lalalilala")
[<la:(0, 2)>, <la:(2, 4)>, <la:(6, 8)>, <la:(8, 10)>]
>>> Rebulk().string('la').matches("LalAlilAla")
[<la:(8, 10)>]
>>> Rebulk().string('la', ignore_case=True).matches("LalAlilAla")
[<La:(0, 2)>, <lA:(2, 4)>, <lA:(6, 8)>, <la:(8, 10)>]
Several patterns can be declared with a single string call.
>>> Rebulk().string('Winter', 'coming').matches("Winter is coming...")
[<Winter:(0, 6)>, <coming:(10, 16)>]
Regular expression patterns¶
Regular expression patterns are based on a compiled regular expression;
re.finditer is used to find
matches. All keyword arguments accepted by
re.compile are supported.
>>> Rebulk().regex(r'l\w').matches("lolita")
[<lo:(0, 2)>, <li:(2, 4)>]
>>> import re # import required for the flags constant
>>> Rebulk().regex('L[A-Z]KERS', flags=re.IGNORECASE) \
... .matches("The LaKeRs are from La")
[<LaKeRs:(4, 10)>]
Each pattern of a single regex call can also be passed as a (pattern, flags) tuple:
>>> Rebulk().regex(('L[A-Z]', re.IGNORECASE), ('L[a-z]KeRs')) \
... .matches("The LaKeRs are from La")
[<La:(20, 22)>, <LaKeRs:(4, 10)>]
The regex backend and repeated captures¶
If the regex module is available and enabled
(REBULK_REGEX_ENABLED=1), rebulk uses it instead of the standard library re module,
and repeated captures are supported (repeated_captures defaults to True). Without it,
force repeated_captures=False to keep behaviour stable.
>>> matches = Rebulk().regex(r'(\d+)(?:-(\d+))+', repeated_captures=False) \
... .matches("01-02-03-04")
>>> matches[0].children
[<01:(0, 2)+initiator=01-02-03-04>, <04:(9, 11)+initiator=01-02-03-04>]
Abbreviations¶
The abbreviations option is a list of 2-tuples; each tuple (old, new) replaces
old with new in the expression before compilation — handy for reusing a separator
class across patterns.
Functional patterns¶
Functional patterns are based on the evaluation of a function. The function receives the
same parameters as Rebulk.matches (the input string) and must return at least the
start and end indices of a Match. It may also return a dict of keyword arguments
for the Match, and may yield multiple matches.
>>> def func(string):
... index = string.find('?')
... if index > -1:
... return 0, index - 11
>>> Rebulk().functional(func).matches("Why do simple ? Forget about it ...")
[<Why:(0, 3)>]
Chain patterns¶
Chain patterns are ordered compositions of string, functional and regex patterns. A
repeater sets repetition on each chain part, similar to regex quantifiers (1, ?,
*, +, {n,m}). Build a chain with chain(), add parts, then close() it.
>>> r = Rebulk().regex_defaults(flags=re.IGNORECASE)\
... .defaults(children=True, formatter={'episode': int, 'version': int})\
... .chain()\
... .regex(r'e(?P<episode>\d{1,4})').repeater(1)\
... .regex(r'v(?P<version>\d+)').repeater('?')\
... .regex(r'[ex-](?P<episode>\d{1,4})').repeater('*')\
... .close()
>>> dict(r.matches("This is E14v2-15-16-17").to_dict())
{'episode': [14, 15, 16, 17], 'version': 2}
Pattern options¶
All patterns accept the following keyword options.
validator- Function to validate the
Matchvalue produced by the pattern. Can be adictto apply a validator to the match named with the key. Reusable validators live inrebulk.validators(usually wired viafunctools.partial). formatter- Function to convert the
Matchvalue. Can be adictto format matches per name. pre_match_processor/post_match_processor- Function taking a single
Match. ReturnFalseto invalidate the match, or aMatchinstance to replace the original one. post_processor- Function
(matches, pattern)to change the default output of the pattern. name- The name of the pattern, propagated to the
Matchobjects it produces. tags- A list of strings qualifying the pattern.
value- Override the
valueproperty of the produced matches. Can be adictkeyed by name. validate_all/format_all- By default the validator/formatter apply to returned matches only; enable these to cover parents and children too.
disabled- A
function(context)that disables the pattern when it returnsTrue. children- If
True, return the childrenMatchobjects instead of a single parent match. private- If
True, the produced matches are internal only and removed at the end ofmatches. private_parent/private_children- Force parent/children matches to be returned but flagged as private.
private_names/ignore_names- Match names to flag as private / to drop from the output after validation.
marker- If
True, the produced matches are markers: excluded from theMatchessequence but available throughMatches.markers(see Rules & processors).
Validator example¶
>>> def check_leap_year(match):
... return int(match.value) in [1980, 1984, 1988]
>>> Rebulk().regex(r'\d{4}', validator=check_leap_year).matches("In year 1982 ...")
[]
>>> Rebulk().regex(r'\d{4}', validator=check_leap_year).matches("In year 1984 ...")
[<1984:(8, 12)>]
Defaults¶
defaults(**kwargs) sets options applied to every subsequent pattern, while
string_defaults, regex_defaults, functional_defaults and chain_defaults scope the
defaults to a single pattern type.
Match¶
A Match is the result produced by a pattern. It exposes a value, position indices
(start, end, span), an optional name, tags, and — for structured patterns —
children matches (each referencing its parent).
When a regular expression defines groups, each group becomes a child Match; a named
group ((?P<name>...)) sets the child's name. The whole match (group(0)) becomes the
parent.
>>> matches = Rebulk() \
... .regex(r"One, (?P<one>\w+), Two, (?P<two>\w+), Three, (?P<three>\w+)") \
... .matches("Zero, 0, One, 1, Two, 2, Three, 3, Four, 4")
>>> matches
[<One, 1, Two, 2, Three, 3:(9, 33)>]
>>> for child in matches[0].children:
... '%s = %s' % (child.name, child.value)
'one = 1'
'two = 2'
'three = 3'
Use children=True to retrieve the children directly, and private_parent /
private_children to customise the returned structure.
Matches¶
Matches holds the result of matches and behaves like a list of Match objects. Every
lookup method accepts an optional predicate callable to filter and an index int to
return a single element. The main methods are:
starting(index, ...)/ending(index, ...)- Matches that start / end at a given index.
previous(match, ...)/next(match, ...)- Matches nearest before / after a match.
named(*names, ...)- Matches having any of the given names (in the order of the names).
tagged(tag, ...)- Matches carrying the given tag.
range(start=0, end=None, ...)- Matches within a range, sorted from start to end.
holes(start=0, end=None, ...)- A hole match for each range where nothing matched.
conflicting(match, ...)- Matches conflicting with the given match.
at_match(match, ...)/at_span(span, ...)/at_index(pos, ...)- Matches at the same position / span / index.
chain_before(position, seps, ...)/chain_after(position, seps, ...)- Chained matches before / after a position, separated only by characters from
seps. names/tags- All match names / tags present in the sequence.
markers- A specialised
Matchessequence holding only marker matches.
>>> matches = Rebulk().regex(r'\d{4}', name="year").string("Big Buck Bunny", name="title") \
... .matches("Big Buck Bunny 2008")
>>> [m.name for m in matches.named("title", "year")]
['title', 'year']
Converting to a dict¶
to_dict(details=False, first_value=False, enforce_list=False) returns an ordered dict
keyed by Match.name. With first_value=True, distinct values for the same name are
wrapped into a list; with enforce_list=True, every value is wrapped into a list for a
predictable, typed shape; with details=True, values are the full Match objects.
Typed retrieval¶
By default Match.value is dynamically typed (Any). For type-safe access, declare a
Key binding a match name to its value type and pass it to a builder method with key=.
The value type is used as the formatter, and reading back through Matches is fully
typed: matches[key] returns T | None and matches.all(key) returns list[T].
>>> from rebulk import Rebulk, Key
>>> year = Key("year", int)
>>> title = Key("title", str)
>>> matches = Rebulk().regex(r'\d{4}', key=year).string('Big Buck Bunny', key=title) \
... .matches("Big Buck Bunny 2008")
>>> matches[year]
2008
>>> matches.all(year)
[2008]
>>> matches[title]
'Big Buck Bunny'
For values not constructible straight from a string, pass an explicit formatter
(a (str) -> T converter):
>>> from datetime import date
>>> released = Key("released", date, formatter=date.fromisoformat)
>>> Rebulk().regex(r'\d{4}-\d{2}-\d{2}', key=released).matches("on 2008-01-02")[released]
datetime.date(2008, 1, 2)
A children=True pattern with several named groups accepts a sequence of keys on the same
key= parameter; each key's converter is registered as a per-name formatter.
>>> season = Key("season", int)
>>> episode = Key("episode", int)
>>> matches = Rebulk().regex(r'S(?P<season>\d+)E(?P<episode>\d+)',
... key=[season, episode], children=True) \
... .matches("Show.S03E07.mkv")
>>> matches[season], matches[episode]
(3, 7)
Declaring keys once¶
To avoid repeating per-name formatters across many patterns, declare the keys once on the
builder with declare_keys. Every pattern built afterwards inherits each key's converter
for the matching group name (a pattern-level formatter still overrides it).
>>> rb = Rebulk().declare_keys(season, episode)
>>> _ = rb.regex(r'S(?P<season>\d+)E(?P<episode>\d+)', children=True)
>>> _ = rb.regex(r'(?P<season>\d+)x(?P<episode>\d+)', children=True)
>>> matches = rb.matches("Show.S03E07.mkv")
>>> matches[season], matches[episode]
(3, 7)
Projecting onto a dataclass or TypedDict¶
Matches.to(...) projects matches onto a typed target. Each field is filled from matches
sharing its name: a list[...] field collects all values, any other field takes the
first, and unmatched fields fall back to their default.
>>> from dataclasses import dataclass, field
>>> @dataclass
... class Movie:
... year: int
... title: str
... tags: list[str] = field(default_factory=list)
>>> tag = Key("tags", str)
>>> matches = Rebulk().regex(r'\d{4}', key=year).string('Big Buck Bunny', key=title) \
... .string('HD', key=tag).string('BluRay', key=tag) \
... .matches("Big Buck Bunny 2008 HD BluRay")
>>> matches.to(Movie)
Movie(year=2008, title='Big Buck Bunny', tags=['HD', 'BluRay'])
to also accepts a TypedDict (unmatched keys are omitted), a primitive type (returns
the first value), or a list[...] of a scalar type (returns all values). A list of a
dataclass or TypedDict is rejected, as a flat match sequence has no record grouping.
Verifying declared keys¶
Keys declared with declare_keys are carried on the resulting Matches (as
matches.declared_keys) and let to close the typing loop: a model field whose type
contradicts a declared key of the same name raises TypeError.
Because a declared key binds by name, a typo or a stale name silently no-ops.
check_keys() returns the declared key names that no built pattern can produce; assert its
result in a test so a typo fails fast. Names produced only by a rule or dynamically by a
functional pattern can be passed to allowed_unused, or declared through a functional
pattern's properties mapping.
>>> rb = Rebulk().declare_keys(Key('season', int), Key('seson', int)) \
... .regex(r'S(?P<season>\d+)', children=True)
>>> rb.check_keys()
['seson']
>>> rb.check_keys(allowed_unused=['seson'])
[]
An opt-in contract check (rebulk.debug.CHECK_DECLARED_KEYS = True, or env
REBULK_CHECK_DECLARED_KEYS=1) additionally asserts at match time that every named value
is an instance of its declared key's value_type. It is off by default (zero production
cost) — enable it in development or CI to catch a formatter override producing the wrong
type.