Greyhorse renders library
Rendering and configuration-parsing support: a small Render engine
abstraction (verbatim copy, or Jinja2 with the jinja extra), a
render-backed YAML/TOML config loader, and a greyhorse.strand
Module/Fragment pair for wiring both into an application.
Two engines, one interface
- the simple engine (key
'') copies a template file verbatim. No third-party dependency -- it is always present, even without thejinjaextra; - the jinja engine (key
'jinja') renders with Jinja2: expressions, loops, theb64encode/b64decode/toYaml/toJsonfilters and areadBinary()global. Only present when thejinjaextra is installed -- an unknown or unavailable key falls back to the simple engine, so calling code never has to branch on what happens to be installed. Runs inside ajinja2.sandbox.ImmutableSandboxedEnvironment: templates can READ values from their context but not mutate them in place ({{ some_list.append(1) }}is refused, same as an unsafe attribute access), and autoescape keys off the template NAME's final suffix only (.html/.htm/.xml) -- a template producing HTML must be named accordingly to get it.
Both come in a sync and an async flavour, handed out by
SyncRenderFactoryImpl/AsyncRenderFactoryImpl for an engine key and a list
of template search directories. A render call returns a Result -- Ok(text)
or an error case that names the file -- never an exception for a missing
template, an unreadable one, or one that is not valid UTF-8. For the Jinja
engine this extends to render-TIME failures too: an undefined variable, the
sandbox refusing an unsafe attribute access, or an ordinary Python exception
raised from inside an expression ({{ 1 / 0 }}) all come back as Err(...)
rather than propagating.
Installation
uv add greyhorse-renders
Add the jinja extra for the Jinja2 engine:
uv add "greyhorse-renders[jinja]"
Without the extra the package still imports and still renders through the
simple engine; it simply reports one fewer engine in
SyncRenderFactoryImpl().keys.
Usage
Every snippet below matches the real API (checked against the test suite as
it was written). Full, runnable programs live in examples/
and are executed by the test suite, so they cannot rot silently.
Direct rendering
from pathlib import Path
from greyhorse_renders.factory import DEFAULT_ENGINE, SyncRenderFactoryImpl
TEMPLATES = Path(__file__).parent / 'templates'
factory = SyncRenderFactoryImpl()
render = factory(DEFAULT_ENGINE, [TEMPLATES])
result = render('greeting.txt')
print(result.unwrap().strip())
render never reads outside TEMPLATES, even given a ..-laden or
absolute template name -- every candidate is confined to the search path it
was found in. Pass 'jinja' instead of DEFAULT_ENGINE for the Jinja2
engine (falls back to the simple one if the jinja extra is not installed).
Loading config files that are also templates
conf.loader renders a file THROUGH an engine before parsing it, so a YAML
or TOML config file may carry template expressions -- but only if the loader
is told which engine to render with: the engine key defaults to '' (the
verbatim/simple engine, which copies its input through unchanged), so
rendering {{ ... }} expressions inside a config file needs 'jinja'
passed explicitly, either as default_render_key at construction or as
render_key on the individual load_yaml/load_toml call:
from pathlib import Path
from pydantic import BaseModel
from greyhorse_renders.conf.loader import SyncPydanticLoader
from greyhorse_renders.factory import SyncRenderFactoryImpl
class Route(BaseModel):
module: str
method: str
loader = SyncPydanticLoader(
doc_schema=Route,
root_dir=Path('config'),
render_factory=SyncRenderFactoryImpl(),
default_render_key='jinja',
)
route = loader.load_yaml(Path('route.yml')).unwrap()
SyncDictLoader/AsyncDictLoader hand back plain dicts instead of a
pydantic model; all four accept load_yaml, load_yaml_list (a multi-
document stream) and load_toml. Malformed input -- an empty document, a
top-level list where a mapping was expected -- comes back as Err(...),
never an uncaught exception.
Two things about the loaders that are easy to assume wrong:
- a value fixed at construction (
values={...}on the loader) OVERRIDES the same key passed as a**kwargson the individualload_yaml/load_tomlcall, not the other way around --deep_update(dict(kwargs), dict(self._values))layers the constructor values on top; root_diris a second template search directory, not a confinement boundary -- aconf_pathargument pointing outsideroot_dirstill loads fine, because the render engine searches[conf_path.parent, root_dir]. This is unlike_ConfinedFileSystemLoader's own guarantee elsewhere in this package (seeprivate/jinja.py), which really does refuse to resolve outside its search paths --root_dir's name invites the same assumption but does not enforce it.
Wiring into an application
RendersModule is a ready-made floor owning both render factories; a
consumer takes one as a constructor parameter and knows nothing about this
library:
from pathlib import Path
from typing import ClassVar
from greyhorse.strand import Resource, running
from greyhorse_renders.abc import SyncRenderFactory
from greyhorse_renders.module import RendersModule
TEMPLATES = Path(__file__).parent / 'templates'
class Greeter:
def __init__(self, render_factory: SyncRenderFactory) -> None:
render = render_factory('', [TEMPLATES])
print(render('greeting.txt').unwrap().strip())
class App(RendersModule):
name = 'my-app'
resources: ClassVar = (Resource(SyncRenderFactory), Resource(Greeter))
with running(App):
pass
An application that already has its own Module lists RendersFragment
and Resource(SyncRenderFactory)/Resource(AsyncRenderFactory) there
directly instead of subclassing RendersModule -- see
examples/03_module.py.
Runnable examples live in examples/ and are executed by the
test suite, so they cannot rot:
uv run python examples/01_render_a_template.py # direct rendering
uv run python examples/02_optional_engine.py # simple vs. jinja fallback
uv run python examples/03_module.py # strand Module/Fragment integration
Development
uv sync
uv run pytest tests -q
uv run mypy greyhorse_renders
Linting and formatting run from the REPOSITORY ROOT, where the shared ruff configuration lives:
ruff check exec/renders
ruff format exec/renders
Metadata
Release files for greyhorse-renders 0.5.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| greyhorse_renders-0.5.6.tar.gz | 70.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| greyhorse_renders-0.5.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 97.8 kB
Release files / greyhorse_renders-0.5.6.tar.gz
| Download URL | greyhorse_renders-0.5.6.tar.gz |
|---|---|
| Size | 70.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8daa72ed4b97998d6172697025b66d5a4e181decfcbc63bce69dbf9137e5f72c
|
|
BLAKE2b-256 checksum How to use checksums |
1c3625b54c60c08e503ec277417c57f1b39fcf6fa7bd5f72468c5a83ee3d6538
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|
Release files / greyhorse_renders-0.5.6-py3-none-any.whl
| Download URL | greyhorse_renders-0.5.6-py3-none-any.whl |
|---|---|
| Size | 27.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f6511383f7a430cec1dd181480c2b2432fa70a4f7d4da8a5892d554aaaf8ebc6
|
|
BLAKE2b-256 checksum How to use checksums |
5ca56c7790f3dfd69931ae3f68499e0e9d9385b8d4360ee14f70ab27ca64ad92
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|