Metadata-Version: 2.5
Name: tplx
Version: 0.1.0
Summary: Render a tree of templates with whichever engine each file's suffix names.
Project-URL: Homepage, https://github.com/jose-pr/tplx/
Project-URL: Documentation, https://jose-pr.github.io/tplx/
Project-URL: Issues, https://github.com/jose-pr/tplx/issues
Author: jose-pr
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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: Programming Language :: Python :: 3.14
Classifier: Topic :: Text Processing :: Markup
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: cli
Requires-Dist: duho<0.8,>=0.7.0; extra == 'cli'
Provides-Extra: dev
Requires-Dist: black; extra == 'dev'
Requires-Dist: build; extra == 'dev'
Requires-Dist: chevron<0.15,>=0.14.0; extra == 'dev'
Requires-Dist: duho<0.8,>=0.7.0; extra == 'dev'
Requires-Dist: jinja2<4,>=3.0; extra == 'dev'
Requires-Dist: mako<2,>=1.1; extra == 'dev'
Requires-Dist: pybars3<0.10,>=0.9.1; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: python-liquid<3,>=2.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material; extra == 'docs'
Requires-Dist: mkdocs<2; extra == 'docs'
Requires-Dist: mkdocstrings[python]; extra == 'docs'
Requires-Dist: pymdown-extensions<12.2; (python_version < '3.11') and extra == 'docs'
Provides-Extra: handlebars
Requires-Dist: pybars3<0.10,>=0.9.1; extra == 'handlebars'
Provides-Extra: jinja
Requires-Dist: jinja2<4,>=3.0; extra == 'jinja'
Provides-Extra: liquid
Requires-Dist: python-liquid<3,>=2.0; extra == 'liquid'
Provides-Extra: mako
Requires-Dist: mako<2,>=1.1; extra == 'mako'
Provides-Extra: mustache
Requires-Dist: chevron<0.15,>=0.14.0; extra == 'mustache'
Description-Content-Type: text/markdown

# tplx

[![Version](https://img.shields.io/pypi/v/tplx.svg)](https://pypi.org/project/tplx/)
[![Python versions](https://img.shields.io/pypi/pyversions/tplx.svg)](https://pypi.org/project/tplx/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/tplx/blob/main/LICENSE)
[![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/tplx/)
[![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/tplx/test.yml)](https://github.com/jose-pr/tplx/actions/workflows/test.yml)

Render a tree of templates where **each file's suffix picks its engine and its
stem names what it renders**: `boot.cfg.j2` is Jinja, `boot.cfg.mako` is mako,
and both answer to `boot.cfg`. A file no engine claims is copied byte for byte.
Documentation: <https://jose-pr.github.io/tplx/>.

## Features

- **One call for every engine** — Jinja, mako, Liquid, Handlebars, Mustache,
  ERB, EPP and a `%{NAME}` shell-style engine, each rendering the same data.
- **Name by stem, dispatch by suffix** — a caller asks for `boot.cfg`; the file
  found decides the engine.
- **Several search roots**, local or remote, searched in order, plus
  per-caller candidate names and paths (`web01.boot.cfg` before `boot.cfg`).
- **Includes that search the same way** — `{% include %}`, `<%include/>`,
  `{{> partial}}` and the rest resolve through the lookup that loaded the file.
- **One undefined-variable policy** (`strict`, `lenient`, `debug`) that means
  the same thing in every engine that can honour it.
- **Your encoding of the data** — a `DataEncoder` subclass decides what is a
  structure and how values are written and flattened for the engines that
  cannot run Python.
- **Byte-exact fallback** — an unclaimed file is copied as it is, never decoded.
- **Pluggable** — an engine is a class with a suffix list and a priority,
  registered directly or through the `tplx.engines` entry point, and checked by
  the shipped `tplx.testing.EngineContract` suite.
- **Nothing required** — each engine's library is an extra, imported only when a
  file needs it.

## Installation

```bash
pip install tplx
```

| Engine | Suffixes | Install | Undefined names | Includes |
| --- | --- | --- | --- | --- |
| Jinja | `.j2` `.jinja` `.jinja2` | `tplx[jinja]` | strict, lenient, debug | `{% include %}` `{% extends %}` `{% import %}` `{% from %}` |
| mako | `.mako` | `tplx[mako]` | strict, lenient (`debug` is lenient) | `<%include/>` `<%inherit/>` `<%namespace/>` |
| Liquid | `.liquid` | `tplx[liquid]` | strict, lenient, debug | `{% include %}` `{% render %}` |
| Handlebars | `.hbs` `.handlebars` | `tplx[handlebars]` | always empty | `{{> name}}` |
| Mustache | `.mustache` | `tplx[mustache]` | always empty | `{{> name}}` |
| ERB | `.erb` | `ruby` on `PATH` | Ruby's `nil` | — |
| EPP | `.epp` | `puppet` on `PATH` | Puppet's `undef` | — |
| shell (`%{NAME}`) | `.shtpl` | — | strict, lenient, debug | — |
| copy | any other | — | n/a: the bytes as stored | — |

Each library-backed engine is an extra of that name (`pip install
"tplx[jinja,mako]"`); the `cli` extra adds the `tplx` command.

Each engine keeps its own syntax and defaults. In particular Handlebars and
Mustache HTML-escape `{{x}}` (`&` becomes `&amp;`), as their specifications
say; write `{{{x}}}` for the value as it is. The other engines write values
unescaped.

Templates and their roots are trusted, in every engine alike: Jinja and
mako run Python, ERB and EPP run Ruby and Puppet. What tplx guards is the
template **name**: one that would leave the search paths (`..`, a backslash,
a drive) is refused.

## Quick start

```python
from tplx import Loader

loader = Loader(["templates"])
text = loader.render("boot.cfg", {"hostname": "web01"}, release="13")
```

A caller with its own search order binds it once; includes follow it:

```python
lookup = loader.lookup(
    candidates=lambda name: [f"web01.{name}", name],
    paths=["debian"],  # templates/debian/ before templates/
)
text = lookup.render("boot.cfg", hostname="web01")
```

Filters, helpers and options go to each engine's own environment:

```python
import shlex

from tplx import JinjaTemplate, Loader

loader = Loader(
    ["templates"],
    configure={JinjaTemplate: lambda env: env.filters.update(quote=shlex.quote)},
)
```

## Command line

```bash
pip install "tplx[cli,jinja]"
tplx render templates/boot.cfg.j2 -s hostname=web01      # one file
tplx render boot.cfg -r templates -d host.json -o boot.cfg  # a name, data from JSON
tplx render web01.boot.cfg boot.cfg -r templates         # the first name found
tplx find boot.cfg -r templates                          # which file, which engine
tplx engines                                             # what is registered
```

`-s a.b=v` nests and merges into `--data`; `-s n=json:3` passes JSON. Exit
status: `0` done, `1` the template or an engine failed, `2` a usage error.

## API overview

| Module | Purpose |
| --- | --- |
| `tplx` | `Loader`, `Lookup`, the engines, the `Template` contract and the registry |
| `tplx.exceptions` | `TplxError` and its subclasses |
| `tplx.engines` | the shipped engine classes |
| `tplx.testing` | `EngineContract`, the suite every engine passes |
| `tplx.cli` | the `tplx` command (`cli` extra) |

## Development

```bash
python -m venv .venv/3.14
.venv/3.14/bin/python -m pip install -e ".[dev,docs]"   # Scripts/ on Windows
.venv/3.14/bin/python -m pytest -q -rs
```

### Releasing

This project follows [Semantic Versioning](https://semver.org/) and keeps a
[`CHANGELOG.md`](https://github.com/jose-pr/tplx/blob/main/CHANGELOG.md).
Pushing a tag matching `v*` triggers the release workflow: test and
dependency-floor gates → build (checking the tag names the version built) →
an installed-wheel smoke test and a strict docs build as gates → GitHub
release → publish. The release workflow never deploys the docs site
itself: for a final release its last job dispatches the docs workflow at the
tag, which owns every Pages deploy.

## License

MIT — see [LICENSE](https://github.com/jose-pr/tplx/blob/main/LICENSE).
