Parameters¶
from wend import P, Param
asset = P("/srv/static") / Param("name")
>>> asset.render(name="logo.png")
'/srv/static/logo.png'
>>> asset.render(name="../../etc/passwd")
Traceback (most recent call last):
...
wend.errors.InvalidBindingError: name='../../etc/passwd' is not a single path segment; declare 'name' with SubPath('name') to bind multi-segment paths
name comes after the head of the expression, so it binds exactly one path segment.
../../etc/passwd is four segments, so the binding is rejected. pathlib joins the same
value as written:
>>> from pathlib import Path
>>> Path("/srv/static") / "../../etc/passwd"
PosixPath('/srv/static/../../etc/passwd')
The four kinds¶
A parameter's kind says what it may bind to when it is used as a path operand:
| kind | constructor | accepts |
|---|---|---|
| path (default at the head) | Param("root"), AnyPath(...) |
any path; pathlib join semantics |
| absolute | Absolute(...) |
an absolute path |
| segment (default after the head) | Param(...), Segment(...) |
exactly one segment: no /, not "", . or .. |
| sub-path | SubPath(...) |
a relative path of zero or more segments, without .. |
A Param takes its kind from its position. At the head of an expression,
with nothing before it, it is a path. Anywhere else it is a segment. The other
constructors fix the kind wherever the parameter is used: AnyPath,
Absolute, Segment and SubPath.
from wend import Absolute, Segment, SubPath, T
root, rel, stem = Param("root"), SubPath("rel"), Param("stem")
module = root / "src" / rel / T(t"{stem}.py")
>>> module
PathExpr('{root}/src/{rel...}/{stem}.py')
>>> module.render(root="/home/a/project", rel="pkg/sub", stem="io")
'/home/a/project/src/pkg/sub/io.py'
>>> module.render(root="project", rel="", stem="setup")
'project/src/setup.py'
>>> module.render(root="project", rel="../..", stem="setup")
Traceback (most recent call last):
...
wend.errors.InvalidBindingError: rel='../..' must not contain '..'
The pattern writes a sub-path as {rel...}. An empty sub-path contributes no segments.
An absolute parameter rejects a relative value:
>>> home = Absolute("home")
>>> (home / ".config").render(home="/home/a")
'/home/a/.config'
>>> (home / ".config").render(home="home/a")
Traceback (most recent call last):
...
wend.errors.InvalidBindingError: home='home/a' must be an absolute path
Kinds apply to path operands. A parameter inside a formatted field, such as {stem} in
t"{stem}.py", contributes formatted text to one segment, and that text cannot contain a
/ (Expressions).
Type, choices and default¶
split = Segment("split", choices={"train", "test"})
idx = Param("idx", type=int)
total = Param("total", type=int, default=100)
shard = P("data") / split / T(t"chunk_{idx:04d}-of-{total:04d}.parquet")
type=is checked withisinstancewhen a value is bound. It also tellsmatchhow to read text back, soidxmatches as anint.ParamandSegmenttaketype=.SubPath,AnyPathandAbsolutebind paths, so they do not.choices=lists the accepted values.matchreads only those values, which can make a path that would otherwise be ambiguous readable in one way (Rendering and matching).default=is the value used when a path is rendered without a binding for the name.
>>> shard.render(split="train", idx=7)
'data/train/chunk_0007-of-0100.parquet'
>>> shard.render(split="val", idx=7)
Traceback (most recent call last):
...
wend.errors.InvalidBindingError: split='val': not one of {'test', 'train'}
>>> shard.match("data/test/chunk_0003-of-0100.parquet")
Match('data/test/chunk_0003-of-0100.parquet', {'split': 'test', 'idx': 3, 'total': 100})
>>> sorted(shard.required_params())
['idx', 'split']
A default is part of the declaration, and is checked against the type, choices and
kind when the parameter is declared. It is used when a path is rendered, and nowhere
else:
bindleaves a defaulted parameter free.defaultsgives the defaults by name, andshard.bind(shard.defaults)applies them.matchreads every part from the path, so a default never makes a part optional.globfinds a defaulted parameter's values on disk, and does not fix it to its default.
>>> shard.bind(shard.defaults)
PathExpr('data/{split}/chunk_{idx:04d}-of-0100.parquet')
>>> print(shard.match("data/test/chunk_0003.parquet"))
None
Two different declarations under one name in one expression raise
ParamConflictError.
Why a segment cannot traverse¶
A segment parameter's value must be exactly one segment: it contains no /, and it is
not "", . or ... Each of those would change the path's structure, so a value bound
to a segment parameter always names one entry of the directory before it. A sub-path
parameter has the same property: its value is relative and contains no ...
So untrusted input, such as a file name from a request, can be bound to a Param after
the head, a Segment or a SubPath: no value bound to it takes the rendered path
outside a literal base before it. SEMANTICS.md §8
records that this is tested with random expressions and operations.
A Param at the head and an AnyPath accept any path, and an Absolute accepts any
absolute path. A value bound to one of them can lead anywhere. To keep such a path below a base,
declare Under on it (Effects).
A joined str is a trusted literal¶
A str joined with / is part of the expression's literal text, with pathlib's rules.
Its .. segments are kept, and an absolute str replaces everything before it:
>>> asset / "../../etc"
PathExpr('/srv/static/{name}/../../etc')
>>> asset / "/etc"
PathExpr('/etc')
Literal text is part of the program. Bind untrusted values to a parameter, and do not join them.