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
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 greyhorse_renders-0.5.5.tar.gz.
File metadata
- Download URL: greyhorse_renders-0.5.5.tar.gz
- Upload date:
- Size: 70.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
35345df72cf890a0f385e17acff251e646bd25a26f84df0ce582b8109356d0c9
|
|
| MD5 |
3008d849d1b2fd68019a90fa01afcc93
|
|
| BLAKE2b-256 |
edb89f650243502dcf84d39c352b2e6c7dbea6185b441d84403216b82e2fbfdb
|
File details
Details for the file greyhorse_renders-0.5.5-py3-none-any.whl.
File metadata
- Download URL: greyhorse_renders-0.5.5-py3-none-any.whl
- Upload date:
- Size: 27.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eccae81637a761f8949d4c922b3da92928e2afcf60cca2ab27eea6bd1f39a482
|
|
| MD5 |
2e11602b531c987c9d414e8190a6dce8
|
|
| BLAKE2b-256 |
3a0db8d8803f97c686af0c118b5b13a66c5c65ffd346b35fc20cd0d633ec74ac
|