Rendering and matching¶
from wend import P, Param, T
idx = Param("idx", type=int)
chunk = P("data") / T(t"chunk_{idx:04d}.parquet")
>>> chunk.render(idx=7)
'data/chunk_0007.parquet'
>>> chunk.match("data/chunk_0007.parquet")
Match('data/chunk_0007.parquet', {'idx': 7})
>>> print(chunk.match("data/chunk_007.parquet"))
None
An expression goes in two directions. render takes bindings
and returns a path. match takes a path and returns the bindings
that render to it, or None when there are none. chunk_007 has no match: {idx:04d}
renders 7 as 0007, and renders no integer as 007.
Rendering¶
renderreturns the canonical path as astr.resolvereturns the same path as apathlib.Path, or as the class given withcls=.
Bindings can be keywords, a mapping keyed by name or by Param, or an object whose
fields are named after the parameters, such as a dataclass or a Pydantic model
(bindings shows what an object binds). Keywords take precedence.
>>> from pathlib import PurePosixPath
>>> chunk.render({idx: 7})
'data/chunk_0007.parquet'
>>> chunk.resolve(idx=7, cls=PurePosixPath)
PurePosixPath('data/chunk_0007.parquet')
>>> chunk.render()
Traceback (most recent call last):
...
wend.errors.MissingBindingError: Missing bindings: ['idx']
A parameter with no binding takes its default if it declares one. Otherwise rendering
raises MissingBindingError. A value that its declaration
or its position does not accept raises InvalidBindingError
(Parameters).
Matching¶
match returns a Match: a read-only mapping of
parameter names to values, with the matched path as .path. The path is normalised as
pathlib normalises it before it is matched, so data//chunk_0007.parquet and
data/./chunk_0007.parquet match too.
Values are typed. A parameter declared with type= is read as that type. An undeclared
one is read as the type its format spec implies: {n:d} and {n:x} read integers, and a
field with no spec reads a str.
>>> n, label = Param("n"), Param("label")
>>> T(t"run_{n:x}").match("run_1f")
Match('run_1f', {'n': 31})
>>> T(t"run_{label}").match("run_1f")
Match('run_1f', {'label': '1f'})
Matches are verified¶
Every candidate reading is rendered again and compared with the path before it is returned, so a match always renders to exactly the path it came from. A format spec's width is a minimum, so a longer number is a valid rendering:
>>> chunk.match("data/chunk_12345.parquet")
Match('data/chunk_12345.parquet', {'idx': 12345})
>>> chunk.render(idx=12345)
'data/chunk_12345.parquet'
glob verifies the paths it finds in the same way.
Ambiguity is an error¶
Some paths can be read in more than one way. match raises
AmbiguousMatchError for them. It does not choose one of
the readings.
stem, ext = Param("stem"), Param("ext")
dist = P("dist") / T(t"{stem}.{ext}")
>>> dist.match("dist/tool.whl")
Match('dist/tool.whl', {'stem': 'tool', 'ext': 'whl'})
>>> dist.match("dist/tool-1.0.tar.gz")
Traceback (most recent call last):
...
wend.errors.AmbiguousMatchError: 'dist/tool-1.0.tar.gz' matches ambiguously: ...
>>> dist.match_all("dist/tool-1.0.tar.gz")
[{'stem': 'tool-1', 'ext': '0.tar.gz'}, {'stem': 'tool-1.0', 'ext': 'tar.gz'}, {'stem': 'tool-1.0.tar', 'ext': 'gz'}]
match_all lists every verified reading. Each reading renders
to the path, so any choice among them would be a guess about which one was meant. The
error's candidates attribute holds the same list.
Declaring what a field can hold, or binding a parameter, leaves one reading:
>>> known = Param("ext", choices={"whl", "tar.gz"})
>>> package = P("dist") / T(t"{stem}.{known}")
>>> package.match("dist/tool-1.0.tar.gz")
Match('dist/tool-1.0.tar.gz', {'stem': 'tool-1.0', 'ext': 'tar.gz'})
>>> dist.bind(ext="gz").match("dist/tool-1.0.tar.gz")
Match('dist/tool-1.0.tar.gz', {'stem': 'tool-1.0.tar'})
Other ambiguous shapes are adjacent string fields such as {a}{b}, and a free head
parameter followed by a SubPath, where the split between the two is not fixed. By
default glob raises AmbiguousMatchError for the first ambiguous path it finds.
ambiguous="skip" leaves such paths out, and a callable receives each error
(wend.glob).
Not invertible¶
An expression that starts with a deferred operation, or a field whose text is derived
from another path, cannot be read back from a path. match raises
NotInvertibleError and names the construct. Binding its
parameters first makes the expression invertible:
>>> from wend import SubPath
>>> root, rel = Param("root"), SubPath("rel")
>>> up = (root / rel).parent
>>> up.match("/srv/a")
Traceback (most recent call last):
...
wend.errors.NotInvertibleError: 'parent({root}/{rel...})' starts with a deferred 'parent'; bind its parameters before matching
>>> up.bind(root="/srv", rel="a/b").match("/srv/a")
Match('/srv/a', {})
A format spec that depends on an unbound parameter, such as {idx:0{width}d}, raises it
too (Expressions).
Reference¶
PathExpr.render,PathExpr.resolve,PathExpr.match,PathExpr.match_all,Match,bindings- SEMANTICS.md §7 (matching) and §3.2 (environments)