A markup template compiler with data support.
Note: The weav package on PyPI is unrelated to this project.
- Render Jinja2 templates with data from YAML, JSON, or TOML files
- Support for multiple data files with deep merge
- Key-value parameters via command line
- Environment variable support (programmatic API)
- Flexible template search paths
- Read data from stdin
- Run commands and use their output as data (
--exec) - Explicit per-source format override (
KEY:FORMAT=SOURCE) - Edit YAML frontmatter in Markdown and reST documents (
weav frontmatter) - Agent skill for AI coding agents (
weav --skill)
uv tool install git+https://github.com/dynamist/weav.gitgit clone https://github.com/dynamist/weav.git
cd weav
uv syncweav has two commands:
| Command | Description |
|---|---|
weav render TEMPLATE |
Render a Jinja2 template with data from files, commands or the environment. |
weav frontmatter FILE |
Edit the YAML frontmatter of a Markdown or reST document. |
Deprecated: the bare
weav TEMPLATE ...form still works and is treated asweav render TEMPLATE ..., but it prints a warning to stderr and will be removed in weav 1.0. A template whose name collides with a command needs the explicit form -- useweav render renderorweav render ./render.
Basic usage with key-value parameters:
weav render template.j2 --keyval name=WorldUsing a YAML data file:
weav render template.j2 --data config.yamlUsing a JSON data file:
weav render template.j2 --data config.jsonUsing a TOML data file:
weav render template.j2 --data config.tomlMixing YAML, JSON, and TOML data files:
weav render template.j2 --data base.yaml --data override.json --data final.tomlMultiple data files with key wrapping:
weav render report.j2 --data items=tasks.yaml --data config.yamlKEY=FILE always namespaces the file's data under KEY — whether it is a
mapping, list, or scalar — so the template accesses it as {{ items }}.
Without a key, mappings merge at the top level of the context and
lists/scalars are wrapped under data.
Reading data from stdin:
cat data.yaml | weav render template.j2 --data ---exec runs a command and uses its standard output as data. This is the way
to combine several query results in one template:
weav render report.j2 \
--exec tasks:yaml='phabfive --format=yaml maniphest search --limit 10' \
--exec pastes:json='phabfive --format=json paste search --limit 10'Each command's stdout is parsed and namespaced under its key, so the template
sees {{ tasks }} and {{ pastes }}. If a command exits non-zero, weav
reports the failure and exits 1 rather than rendering an incomplete document.
The command's own stderr is passed through untouched.
Commands run without a shell, so pipes, redirects and globs are not
interpreted — the command string is split into arguments the way a shell would
split it, then executed directly. When you need a pipeline, use process
substitution with --data instead:
weav render report.j2 --data tasks=<(phabfive --format=yaml maniphest search | head -50)Note that quoting is consumed twice: once by your shell, once by weav. A command containing quotes needs them escaped or nested.
Data files pick a parser from their suffix, which does not work for stdin,
extensionless files, or --exec commands. Prefix any spec with :FORMAT to be
explicit:
weav render template.j2 --data config:yaml=/dev/fd/63 # extensionless path
weav render template.j2 --data :json=- # stdin as JSON
weav render template.j2 --exec items:json='some-query' # command output as JSONThe full spec grammar for --data and --exec is [KEY][:FORMAT]=SOURCE,
where FORMAT is yaml, json or toml. A KEY= prefix is only recognised
when KEY looks like a name, so paths and commands containing = (such as
phabfive --format=yaml ...) are left intact.
Using environment variables:
# Load all MYAPP_* environment variables
export MYAPP_NAME=World
export MYAPP_DEBUG=true
weav render template.j2 --env MYAPP_
# Variables are available as lowercase keys: {{ name }}, {{ debug }}
# Combine with data files (env vars override file values)
weav render template.j2 --data config.yaml --env MYAPP_Sources are merged in a fixed order, with later sources overriding earlier ones:
--datafiles, in command-line order--execcommands, in command-line order--envprefixes--keyvalpairs
Templates are searched in the following order:
- Package bundled templates
./templatesin current directory~/.local/share/weav/templates(user data directory)~/Documents/weav/templates(user documents)
You can also specify a direct file path to a template.
| Option | Description |
|---|---|
-d, --data |
YAML/JSON/TOML data file(s). Use KEY=FILE to wrap under key, KEY:FORMAT=FILE to force a format. Use - for stdin. |
-x, --exec |
Run a command and use its stdout as data. Same KEY[:FORMAT]= syntax. Runs without a shell. Can specify multiple times. |
-e, --env |
Environment variable prefix (e.g., MYAPP_). Can specify multiple times. |
-k, --keyval |
Key-value pairs (KEY=VAL). Can specify multiple times. |
-v, --verbose |
Show verbose output (loaded files, etc.) |
-V, --version is a global option: use weav --version.
| Option | Description |
|---|---|
-u, --upsert |
Insert or update KEY=VAL. Comma-separated pairs allowed. Can specify multiple times. |
-D, --delete |
Delete KEY. Comma-separated keys allowed. Can specify multiple times. |
--stdout |
Write the result to stdout instead of editing the file in place. |
-v, --verbose |
Report inserted, updated and deleted keys on stderr. |
weav --skill prints a skill file describing the CLI for AI coding agents. It
goes to stdout verbatim, with nothing on stderr, so it can be redirected
straight into place:
mkdir -p ~/.claude/skills/weav
weav --skill > ~/.claude/skills/weav/SKILL.mdEvery help page ends with a short resources block pointing at the flag, so an
agent that only reads --help still finds it.
weav frontmatter adds, changes and removes keys in a document's YAML
frontmatter block, preserving comments, quoting, block scalars, the document's
own indentation and its long values, which are never re-wrapped.
# Stamp a document with the commit it was derived from
weav frontmatter contract.md --upsert origin=$(git rev-parse HEAD)
# Remove several keys at once
weav frontmatter doc.md --delete status,docid
# Preview without touching the file
weav frontmatter doc.md --upsert lorem=ipsum --stdoutThe file is edited in place by default. Pass --stdout to print the
result and leave the file untouched.
A frontmatter block is recognised only when the document opens with a ---
line, and the next --- or ... line closes it. A --- used as a thematic
break in the body is left alone, as is an indented --- or a longer ----,
and a document with no frontmatter simply gains a block.
The opening delimiter is required, which means a Markdown setext heading is
never mistaken for metadata -- Overview: the big picture underlined by ---
stays a heading. It also means a document whose frontmatter is closed by ---
but never opened by one is treated as body content; add a leading --- to such
documents. Documents are read and written as UTF-8, and a byte order mark, CRLF
line endings and the exact delimiter lines all survive a round trip. A document
whose content did not change is not rewritten at all.
weav is a library as well as a command. The supported names are exported from the top level:
from weav import FrontmatterDocument, compile_templateEvery public name is resolved lazily, so import weav pulls in no third-party
module and costs about a millisecond -- a consumer who only wants
FrontmatterDocument never imports Jinja2. The module holding a name is
imported the first time the name is touched.
| Documents | FrontmatterDocument |
| Templates | compile_template, find_template, get_template_paths |
| Data sources | ContextBuilder, DataSource, YamlDataSource, JsonDataSource, TomlDataSource, StdinDataSource, ExecDataSource, KeyvalDataSource, EnvDataSource |
| Errors | FrontmatterError, DuplicateKeyError, TemplateError, DataSourceError |
| Utilities | deep_merge |
Everything else stays in its own module -- weav.datasources.parse_data_spec,
weav.utils.mangle_keyval, weav.frontmatter.detect_indent -- because their
contracts are the CLI's argument grammar or the YAML emitter's internals rather
than a library API. Submodule imports keep working and are not deprecated;
from weav.frontmatter import FrontmatterDocument is as supported as the short
form.
from pathlib import Path
from weav import FrontmatterDocument
doc = FrontmatterDocument.from_file(Path("contract.md"))
doc.patch(upsert={"origin": "deadbeef"}, delete=["draft"])
doc.write(Path("contract.md"))patch() reports what it did, as {"inserted": [...], "updated": [...], "deleted": [...]}, and write() returns False without touching the file
when the bytes would not change. A document already in memory goes through the
constructor instead of from_file():
doc = FrontmatterDocument(text)
doc.patch(upsert={"origin": "deadbeef"})
print(doc.frontmatter["origin"], doc.content, doc.dumps())The constructor decodes exactly as from_file() does: a leading byte order
mark is recorded and stripped, and CRLF or lone-CR endings are folded, so the
same bytes give the same document whichever way they arrive. dumps() returns
the document with \n endings and no BOM; write() is what restores the
original endings and the mark.
doc.frontmatter is a ruamel.yaml CommentedMap, which is a dict that
also remembers comments and quoting. Values are mostly what you would expect --
unquoted scalars come back as plain str and int, and quoted ones as
str subclasses that compare and serialise normally. The exception worth
knowing is that an unquoted ISO date becomes a datetime.date, so
json.dumps(dict(doc.frontmatter)) raises TypeError on a document with one;
quote the value or convert it before serialising.
from pathlib import Path
from weav import (
ContextBuilder,
EnvDataSource,
ExecDataSource,
JsonDataSource,
KeyvalDataSource,
TomlDataSource,
YamlDataSource,
compile_template,
)
builder = ContextBuilder()
builder.add(YamlDataSource(Path("base.yaml")))
builder.add(JsonDataSource(Path("override.json")))
builder.add(TomlDataSource(Path("settings.toml")))
builder.add(ExecDataSource("phabfive --format=yaml paste search", "pastes"))
builder.add(EnvDataSource(prefix="MYAPP_")) # Read MYAPP_* env vars
builder.add(KeyvalDataSource(["debug=true"]))
context = builder.build()
# Or use the high-level API
result = compile_template(
"template.j2",
data_files=["config.yaml", "data.json", "settings.toml"],
keyvals=["name=World"],
exec_commands=["pastes:yaml=phabfive --format=yaml paste search"],
)compile_template() takes the same specification strings the CLI accepts,
rather than structured arguments.
| Class | Description |
|---|---|
YamlDataSource |
Load data from YAML files |
JsonDataSource |
Load data from JSON files |
TomlDataSource |
Load data from TOML files |
EnvDataSource |
Load data from environment variables |
KeyvalDataSource |
Load data from key=value strings |
StdinDataSource |
Load data from standard input |
ExecDataSource |
Run a command and load data from its stdout |
DataSource |
The protocol the others satisfy |
DataSource is a typing Protocol and is not runtime_checkable: implement
it structurally -- a name property and a load() returning a dict -- rather
than inheriting from it, and do not use it with isinstance().
from weav import DataSourceError, FrontmatterError, TemplateErrorFrontmatterError is raised only when a document that opens with --- has a
block that will not parse. A document with no frontmatter is not an error.
It carries line and column, 1-based and counted from the top of the
document, opening --- included -- the position an editor shows -- and
problem, a one-line description. Either position is None when the YAML
parser reported none. str() is the parser's full message.
A key that appears twice in the same mapping, at any depth, raises
DuplicateKeyError, a subclass of FrontmatterError. It adds key, as spelled
in the source, and first_line, the line of the key it repeats. A duplicate is
refused rather than resolved, because last-value-wins would load the document
with the wrong value in it.
from weav import DuplicateKeyError, FrontmatterDocument
try:
FrontmatterDocument.from_file(path)
except DuplicateKeyError as exc:
print(f"{path}:{exc.line}: {exc.key!r} is already set on line {exc.first_line}")TemplateError covers every way Jinja2 can fail a template: not found,
malformed, an unknown filter, an undefined variable, a missing include. The
Jinja2 exception is kept as __cause__, so there is no need to import or catch
jinja2 alongside it. An exception raised by the template's own expressions,
such as ZeroDivisionError from {{ 1 / 0 }}, is not wrapped and propagates
as itself.
weav is published to GitHub Releases, not to PyPI, and the weav package on
PyPI is an unrelated project. Depend on it by direct reference:
weav @ git+https://github.com/dynamist/weav.git@v0.3.0
uv pip compile ignores [tool.uv.sources], so a project that pins weav
there also needs the direct reference in its requirements input, plus a
weav>=0.3.0 floor. Without both, the resolve can silently pick up the
unrelated PyPI package instead.
Apache License 2.0 - Copyright 2026 Dynamist AB