A tiny, generic Python codegen orchestrator.
Project description
evergen
evergen is a tiny, zero-dependency Python codegen orchestrator: a generator is
any Python file that exposes gen() -> str, and evergen executes it, signs the
generated body with a header, and writes the mapped output file. Generated
files are real code that you commit; the signed header lets evergen tell a
clean generated file from a stale one from one you edited by hand, so
regeneration is safe by mechanism instead of by discipline.
Quick start
Run evergen with uvx:
uvx evergen --help
Or from a local clone: uv tool run --from /path/to/evergen evergen …. The
examples below assume a clone and run from examples/.
Hello world
examples/hello/ contains one generator file,
hello.eg.py:
def gen():
return 'print("hello world")\n'
From examples/hello/, run:
uv tool run --from ../.. evergen --output '{}.py' '{}.eg.py'
WROTE hello.py <- hello.eg.py
The output hello.py is a committed, runnable file:
# @generated by evergen from hello.eg.py — SignedHash<<4660ab1ff310887b>> — do not hand-edit
print("hello world")
The input pattern's {} captures hello (the .eg generator suffix is
stripped), and that capture fills the {} in --output.
The three states
The SignedHash<<…>> token is the mechanism. When evergen is about to write a
target that already exists, it verifies the stored hash against the file's
body and acts on the result:
| Existing target | State | Without --overwrite |
With --overwrite |
|---|---|---|---|
| Header present, hash matches body | clean | overwrite | overwrite |
No SignedHash<<…>> token |
unmanaged | refuse: not generated by evergen; use --overwrite to replace |
replace |
| Header present, hash does not match | dirty (hand-edited) | refuse: hand-edited; keep your edits by removing the header and deleting the generator, or discard them with --overwrite |
replace |
A missing target is simply written. Refusals exit nonzero after reporting every target, so nothing you hand-edited is ever silently destroyed.
--check and pre-commit
--check writes nothing and exits nonzero if any target is not clean and
current:
MISSING: the output file does not exist.UNMANAGED: the output has noSignedHash<<…>>token.DIRTY: the output has a token but its signed hash no longer matches its body.STALE: the hash is valid, but rerunning the generator would produce different body bytes.
DIRTY vs STALE is the useful distinction in CI: dirty means a human edited
generated output (their edits are at risk); stale means the generator changed
and someone forgot to rerun evergen.
Pre-commit hook:
repos:
- repo: local
hooks:
- id: evergen-check
name: evergen check
entry: uv tool run --from /path/to/evergen evergen --check --output '{}.py' '{}.eg.py'
language: system
pass_filenames: false
Graduation
When a generated file has diverged enough that you want to hand-own it, graduate it: delete the header line from the output and delete the generator file. The output is now ordinary source code; evergen has no further claim on it. This is the intended exit ramp — the dirty-state refusal message points here.
Bring your own templates (Jinja2 example)
evergen does not depend on Jinja2 and never will. If you want templates, make
Jinja2 your project's dependency and call it from your generator — evergen
only sees gen() -> str. examples/jinja_family/
is the full runnable version; the shape:
templates/
model.j2.py
models.eg.py
templates/model.j2.py:
class {{ name }}:
table = "{{ table }}"
models.eg.py:
from pathlib import Path
from jinja2 import Template
HERE = Path(__file__).parent
ROWS = [
{"name": "User", "table": "users"},
{"name": "Invoice", "table": "invoices"},
]
def render(template_name, **data):
text = (HERE / "templates" / template_name).read_text()
return Template(text).render(**data)
def gen():
return "\n\n".join(render("model.j2.py", **row) for row in ROWS) + "\n"
Run with the dependency supplied at the call site:
uv tool run --from /path/to/evergen --with jinja2 evergen --output '{}.py' '{}.eg.py'
WROTE models.py <- models.eg.py
CLI reference
evergen --output OUT_PATTERN [--check] [--overwrite] [--header TEMPLATE] INPUT_PATTERN [INPUT_PATTERN ...]
Input and output patterns
INPUT_PATTERN is a glob containing exactly one {} placeholder. The text
matched by {} is the capture and is substituted into OUT_PATTERN, which
also must contain exactly one {}.
As a convenience, an input may also be a plain .py file path with no glob
characters and no {}. Its capture is the filename minus the final .py and
minus a trailing generator suffix of .eg, .gen, or .generator when
present. For example, summary.eg.py maps with capture summary.
Generator files are loaded by file path with importlib spec-from-file
machinery, never by package import. Dotted filenames such as summary.eg.py
work.
Output path resolution:
- If
OUT_PATTERNis absolute, it is used as an absolute path. - If
OUT_PATTERNis relative and the matchingINPUT_PATTERNcontains**, the output path is resolved relative to the matched generator file's directory. For example,src/**/{}.eg.pywith--output {}__out.pymapssrc/pkg/user.eg.pytosrc/pkg/user__out.py. - Otherwise, a relative
OUT_PATTERNis resolved relative to the current working directory.
Processing order is deterministic: matched generator paths are sorted before execution. If two inputs map to the same output path, evergen exits with a hard error before writing any file.
--header TEMPLATE
The default header line is:
# @generated by evergen from {source} — SignedHash<<{hash}>> — do not hand-edit
{source} is the generator path relative to the output file. {hash} is the
first 16 hex characters of SHA-256 over the generated body, excluding the
header line. Newlines are normalized to \n before hashing, so CRLF checkouts
do not create false dirty reports.
--header TEMPLATE replaces the header line. The template must be one line
and must contain {hash}; {source} is optional.
Design notes
Determinism law. gen() must be deterministic: the same repository state
must produce the same bytes. evergen documents this law but does not enforce
it. (Internally evergen also refuses to generate from stale bytecode — it
compiles generator source directly instead of going through the __pycache__
machinery.)
Trust model. Generator files are trusted code. evergen loads and executes
them with the same trust model as setup.py; only run generators from
repositories you trust.
SignedHash detection invariant. Detection is decoration-independent: the
machine-invariant marker is the literal token SignedHash<<…>>, and custom
--header templates must place the digest in SignedHash<<{hash}>>. evergen
detects the token by scanning the first five lines of an existing file, and
hash verification covers everything after the line containing the token.
Atomic writes. Each output is written to a sibling temporary file and then renamed into place, so a crash mid-write cannot truncate an existing output.
Path-resolution rule. Relative outputs follow the ** rule above:
recursive patterns anchor outputs next to their generators; everything else
anchors to the current working directory.
Name
PyPI project: https://pypi.org/project/evergen/
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file evergen-0.1.0.tar.gz.
File metadata
- Download URL: evergen-0.1.0.tar.gz
- Upload date:
- Size: 23.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
209ecc79451a13fe353da59bdde22d4600df783a43661e9ae7f340f08cc1a33c
|
|
| MD5 |
5a86c2d2c6bb789254477798f7aa46d6
|
|
| BLAKE2b-256 |
494c7cfba89400fecb45733a641818c853395efd991dffe4b182541258866552
|
Provenance
The following attestation bundles were made for evergen-0.1.0.tar.gz:
Publisher:
publish.yml on Elijas/evergen
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
evergen-0.1.0.tar.gz -
Subject digest:
209ecc79451a13fe353da59bdde22d4600df783a43661e9ae7f340f08cc1a33c - Sigstore transparency entry: 2083586899
- Sigstore integration time:
-
Permalink:
Elijas/evergen@5598f0036b2b3971211109904e20595d83a7347f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Elijas
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5598f0036b2b3971211109904e20595d83a7347f -
Trigger Event:
push
-
Statement type:
File details
Details for the file evergen-0.1.0-py3-none-any.whl.
File metadata
- Download URL: evergen-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c91fea64060a564492d277ced000435bd9092497f4f10483aa1c2f19c34a6686
|
|
| MD5 |
0ef27d910f97315cc39b857bbf00dd45
|
|
| BLAKE2b-256 |
03e12c2c773719f6dd0e517cc2600fa8f6b3852aa0ae8d4813159890a89983f5
|
Provenance
The following attestation bundles were made for evergen-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Elijas/evergen
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
evergen-0.1.0-py3-none-any.whl -
Subject digest:
c91fea64060a564492d277ced000435bd9092497f4f10483aa1c2f19c34a6686 - Sigstore transparency entry: 2083586936
- Sigstore integration time:
-
Permalink:
Elijas/evergen@5598f0036b2b3971211109904e20595d83a7347f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Elijas
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5598f0036b2b3971211109904e20595d83a7347f -
Trigger Event:
push
-
Statement type: