Skip to main content

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 no SignedHash<<…>> 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_PATTERN is absolute, it is used as an absolute path.
  • If OUT_PATTERN is relative and the matching INPUT_PATTERN contains **, the output path is resolved relative to the matched generator file's directory. For example, src/**/{}.eg.py with --output {}__out.py maps src/pkg/user.eg.py to src/pkg/user__out.py.
  • Otherwise, a relative OUT_PATTERN is 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

evergen-0.1.0.tar.gz (23.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

evergen-0.1.0-py3-none-any.whl (12.0 kB view details)

Uploaded Python 3

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

Hashes for evergen-0.1.0.tar.gz
Algorithm Hash digest
SHA256 209ecc79451a13fe353da59bdde22d4600df783a43661e9ae7f340f08cc1a33c
MD5 5a86c2d2c6bb789254477798f7aa46d6
BLAKE2b-256 494c7cfba89400fecb45733a641818c853395efd991dffe4b182541258866552

See more details on using hashes here.

Provenance

The following attestation bundles were made for evergen-0.1.0.tar.gz:

Publisher: publish.yml on Elijas/evergen

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for evergen-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c91fea64060a564492d277ced000435bd9092497f4f10483aa1c2f19c34a6686
MD5 0ef27d910f97315cc39b857bbf00dd45
BLAKE2b-256 03e12c2c773719f6dd0e517cc2600fa8f6b3852aa0ae8d4813159890a89983f5

See more details on using hashes here.

Provenance

The following attestation bundles were made for evergen-0.1.0-py3-none-any.whl:

Publisher: publish.yml on Elijas/evergen

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page