Operations¶
from wend import Param, SubPath, T
root, split, rel = Param("root"), Param("split"), SubPath("rel")
idx = Param("idx", type=int)
shard = root / split / T(t"chunk_{idx:04d}.parquet")
source = root / rel
>>> shard.parent
PathExpr('{root}/{split}')
>>> shard.with_suffix(".json")
PathExpr('{root}/{split}/chunk_{idx:04d}.json')
>>> source.parent
PathExpr('parent({root}/{rel...})')
shard's last segment is known from its structure, so its parent and its suffix are
computed with no values bound. source ends in a sub-path of zero or more segments, so
its parent depends on the value of rel, and the operation waits for it.
Every operation is one of three kinds: static, deferred or rejected.
Static operations¶
A static operation is computed from the structure, without bindings:
parent,nameandwith_nameof an expression whose last part is a segment;stem,suffixandwith_suffix, when the last dot of that segment is in literal text, no field after it can contain a dot (an integer field cannot), and a character other than a dot comes before it;relative_toa structural prefix;bind.
>>> shard.name
Text('chunk_{idx:04d}.parquet')
>>> shard.stem
Text('chunk_{idx:04d}')
>>> shard.suffix
Text('.parquet')
>>> shard.with_name("index.json")
PathExpr('{root}/{split}/index.json')
>>> shard.relative_to(root)
PathExpr('{split}/chunk_{idx:04d}.parquet')
name, stem and
suffix are strings, so they return a Text: a
symbolic string with its own render.
A static operation can remove parameters. shard.parent.parent is {root}: the value of
split does not change it, so the result has no split and needs no binding for it.
>>> shard.parent.parent
PathExpr('{root}')
>>> shard.parent.parent.render(root="data")
'data'
Deferred operations¶
A deferred operation depends on values that are not bound yet. It is recorded in the expression, shown in the pattern as a call, and evaluated with pathlib once its inputs are bound:
>>> source.parent.render(root="/srv", rel="a/b")
'/srv/a'
>>> source.parent.render(root="/srv", rel="")
'/'
With rel="" the path is /srv, so its parent is /, as pathlib computes it.
The suffix of a free-form string field is deferred too, since the value can contain a dot:
>>> name = Param("name")
>>> upload = root / "uploads" / name
>>> upload.suffix
Text('{suffix({root}/uploads/{name})}')
>>> upload.suffix.render(root="data", name="photo.tar.gz")
'.gz'
The suffix of {name}.csv is deferred for a similar reason. With name="" the segment
is .csv, a hidden file whose suffix is "":
>>> (root / T(t"{name}.csv")).suffix
Text('{suffix({root}/{name}.csv)}')
>>> (root / T(t"report_{name}.csv")).suffix
Text('.csv')
An expression that starts with a deferred operation cannot be matched against a path until its parameters are bound (Rendering and matching).
Rejected operations¶
An operation that is invalid for every binding raises
PathOperationError when the expression is built. It is a
ValueError, as pathlib's error for the same operation is.
>>> from wend import P
>>> P("/").with_name("x")
Traceback (most recent call last):
...
wend.errors.PathOperationError: '/' has an empty name
>>> shard.with_suffix("json")
Traceback (most recent call last):
...
wend.errors.PathOperationError: Invalid suffix 'json'
bind is partial evaluation¶
bind substitutes some parameters and folds what becomes static.
The others stay free.
>>> shard.bind(root="data")
PathExpr('data/{split}/chunk_{idx:04d}.parquet')
>>> shard.bind(root="data", split="train", idx=3).is_static
True
A value can also be an expression, which places the path under another one, or a
Param, which renames:
>>> shard.bind(root=P("/mnt") / Param("user"))
PathExpr('/mnt/{user}/{split}/chunk_{idx:04d}.parquet')
>>> shard.bind(split=Param("part"))
PathExpr('{root}/{part}/chunk_{idx:04d}.parquet')
bind does not apply defaults (Parameters).
render equals the pathlib computation¶
For every binding that an expression and its sub-expressions accept, render returns the
path that the eager pathlib computation of the same operations returns, applied to the
formatted strings:
>>> from pathlib import PurePosixPath
>>> meta = shard.with_suffix(".json").relative_to(root)
>>> meta.render(root="data", split="train", idx=3)
'train/chunk_0003.json'
>>> eager = PurePosixPath("data") / "train" / f"chunk_{3:04d}.parquet"
>>> str(eager.with_suffix(".json").relative_to("data"))
'train/chunk_0003.json'
A binding that would make pathlib change the path's structure without an error is
rejected with InvalidBindingError:
- a separator inside a segment;
- an empty segment, or a
.segment; - an escaping
..; - a value that violates a declared type or choices.
Evaluation is non-strict. A part of an expression that an operation discards, such as the
segment parent drops or everything before an absolute join, need not be evaluated, so a
value bound only to that part may go unchecked. This is the one case where render can
return a path for a binding that the eager computation rejects.
Reference¶
PathExpr,Text,PathOperationError- SEMANTICS.md §5 (operations) and §6 (the reference value and wend's obligations)