Metadata-Version: 2.4
Name: evergen
Version: 0.1.0
Summary: A tiny, generic Python codegen orchestrator.
Project-URL: Repository, https://github.com/Elijas/evergen
Project-URL: Changelog, https://github.com/Elijas/evergen/blob/main/CHANGELOG.md
Author: Elijas
License-Expression: MIT
License-File: LICENSE
Keywords: cli,code-generation,codegen,generator,scaffolding
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# 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`:

```sh
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/`](examples/).

### Hello world

[`examples/hello/`](examples/hello/) contains one generator file,
`hello.eg.py`:

```python
def gen():
    return 'print("hello world")\n'
```

From `examples/hello/`, run:

```sh
uv tool run --from ../.. evergen --output '{}.py' '{}.eg.py'
```

```text
WROTE hello.py <- hello.eg.py
```

The output `hello.py` is a committed, runnable file:

```python
# @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:

```yaml
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/`](examples/jinja_family/)
is the full runnable version; the shape:

```text
templates/
  model.j2.py
models.eg.py
```

`templates/model.j2.py`:

```jinja2
class {{ name }}:
    table = "{{ table }}"
```

`models.eg.py`:

```python
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:

```sh
uv tool run --from /path/to/evergen --with jinja2 evergen --output '{}.py' '{}.eg.py'
```

```text
WROTE models.py <- models.eg.py
```

## CLI reference

```sh
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:

```text
# @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/>
