Skip to main content

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 the jinja extra;
  • the jinja engine (key 'jinja') renders with Jinja2: expressions, loops, the b64encode/b64decode/toYaml/toJson filters and a readBinary() global. Only present when the jinja extra 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 a jinja2.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 **kwargs on the individual load_yaml/load_toml call, not the other way around -- deep_update(dict(kwargs), dict(self._values)) layers the constructor values on top;
  • root_dir is a second template search directory, not a confinement boundary -- a conf_path argument pointing outside root_dir still loads fine, because the render engine searches [conf_path.parent, root_dir]. This is unlike _ConfinedFileSystemLoader's own guarantee elsewhere in this package (see private/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)

Source distribution for greyhorse-renders 0.5.6
File Size Uploaded
greyhorse_renders-0.5.6.tar.gz 70.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for greyhorse-renders 0.5.6
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

0.5.8

2 release files

0.5.7

2 release files

This release

0.5.6 This release

2 release files

0.5.5

2 release files

0.4.23

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page