Polars¶
wend.frame.resolve_frame renders one expression for every
row of a Polars DataFrame whose columns are named after the expression's parameters. It
checks every row with the same rules as render, then builds
the paths with Polars' own string operations. This page needs Python 3.14 and the
polars extra:
pip install "wend[polars]"
resolve_frame is one way to render many paths. A compiled renderer's many method
renders any expression from rows of bindings, without Polars
(Datasets and shards). The examples
build on each other: run them in order.
Render a column of paths¶
import polars as pl
from wend import Param, Segment, counter
from wend.frame import resolve_frame
root = Param("root")
split = Segment("split", choices={"train", "test"})
filename = counter("shard", width=5, of_total=True, sep="-", suffix=".parquet")
shard = root / split / filename
frame = pl.DataFrame(
{
"root": ["data", "data", "/mnt/data", "/mnt/data"],
"split": ["train", "train", "test", "test"],
"idx": [0, 1, 0, 1],
"total": [2, 2, 2, 2],
}
)
resolve_frame returns a String Series named path, with one path per row:
>>> paths = resolve_frame(shard, frame)
>>> paths.name, paths.dtype
('path', String)
>>> for path in paths:
... print(path)
data/train/shard-00000-of-00002.parquet
data/train/shard-00001-of-00002.parquet
/mnt/data/test/shard-00000-of-00002.parquet
/mnt/data/test/shard-00001-of-00002.parquet
Each path is the one render returns for that row:
>>> paths.to_list() == [shard.render(row) for row in frame.iter_rows(named=True)]
True
>>> frame.with_columns(path=paths).columns
['root', 'split', 'idx', 'total', 'path']
A LazyFrame is accepted too. The result is collected.
Path columns are normalised¶
A column at a path position, such as root here, is normalised as render normalises
a path: data/, ./data and /mnt//data become data, data and /mnt/data. A
column whose values are all normalised already skips that work.
>>> messy = frame.with_columns(
... pl.Series("root", ["data/", "./data", "/mnt//data", "/mnt/./data"])
... )
>>> resolve_frame(shard, messy).to_list() == paths.to_list()
True
Missing columns take defaults¶
A parameter with no column takes its default on every row, as render does for a
missing binding:
>>> total = Param("total", type=int, default=2)
>>> sized_filename = counter(
... "shard", width=5, of_total=True, sep="-", suffix=".parquet", total=total
... )
>>> sized = root / split / sized_filename
>>> resolve_frame(sized, frame.drop("total")).to_list() == paths.to_list()
True
Every row is checked¶
A value that render rejects raises the same exception class, with the number of the
first row that fails. No paths are returned:
>>> bad = frame.with_columns(pl.Series("split", ["train", "val", "test", "test"]))
>>> resolve_frame(shard, bad)
Traceback (most recent call last):
...
wend.errors.InvalidBindingError: Row 1: split not in choices
>>> shard.render(root="data", split="val", idx=1, total=2)
Traceback (most recent call last):
...
wend.errors.InvalidBindingError: split='val': not one of {'test', 'train'}
A null is always rejected, also where render would format None as text:
>>> from wend import P, T
>>> name = Param("name")
>>> note = P("notes") / T(t"note_{name}.txt")
>>> note.render(name=None)
'notes/note_None.txt'
>>> resolve_frame(note, pl.DataFrame({"name": ["a", None]}))
Traceback (most recent call last):
...
wend.errors.InvalidBindingError: Row 1: name is null
What it refuses¶
A lowering to Polars exists only where Polars reproduces render exactly. For anything
else, resolve_frame raises NotImplementedError, which names the construct. Float
columns are refused:
>>> score = Param("score")
>>> checkpoint = root / T(t"model-{score:.3f}.pt")
>>> scores = pl.DataFrame({"root": ["runs", "runs"], "score": [0.91234, 0.8]})
>>> resolve_frame(checkpoint, scores)
Traceback (most recent call last):
...
NotImplementedError: Column 'score' of type Float64 cannot be lowered to Polars exactly; use expr.compile().many(rows)
So is !r on a string, which renders the value's repr:
>>> quoted = root / T(t"{name!r}.txt")
>>> resolve_frame(quoted, pl.DataFrame({"root": ["out"], "name": ["a"]}))
Traceback (most recent call last):
...
NotImplementedError: {name!r} (!r on a string) cannot be lowered to Polars exactly; use expr.compile().many(rows)
The fallback renders the same rows with the compiled renderer. iter_rows(named=True)
gives one mapping per row:
>>> checkpoint.compile().many(scores.iter_rows(named=True))
['runs/model-0.912.pt', 'runs/model-0.800.pt']
The wend.frame docstring lists exactly what is lowered and what is
refused. In outline:
- Lowered: String, integer and Boolean columns; integer format specs in bases
d,b,o,xandX, with fill, alignment, sign,#,0, width and grouping; string specs with fill, alignment, width and precision; andparent,with_name,with_stem,with_suffixand joins applied to unbound paths. - Refused: floats and other column types;
!rand!aon strings; format specs that contain a parameter;relative_toon unbound paths; and a declaredtype=that a column's values are not instances of.
to_polars returns the Polars expression itself, for use inside
a larger query. It does not validate: rows that render would reject get unspecified
values.
See also¶
- Datasets and shards:
compile,manyandcompile_many. - Parameters: the checks every row goes through.
- API reference:
wend.frame,resolve_frame,to_polars,PathExpr.compile.