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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

greyhorse_renders-0.5.5.tar.gz (70.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

greyhorse_renders-0.5.5-py3-none-any.whl (27.8 kB view details)

Uploaded Python 3

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

Hashes for greyhorse_renders-0.5.5.tar.gz
Algorithm Hash digest
SHA256 35345df72cf890a0f385e17acff251e646bd25a26f84df0ce582b8109356d0c9
MD5 3008d849d1b2fd68019a90fa01afcc93
BLAKE2b-256 edb89f650243502dcf84d39c352b2e6c7dbea6185b441d84403216b82e2fbfdb

See more details on using hashes here.

File details

Details for the file greyhorse_renders-0.5.5-py3-none-any.whl.

File metadata

File hashes

Hashes for greyhorse_renders-0.5.5-py3-none-any.whl
Algorithm Hash digest
SHA256 eccae81637a761f8949d4c922b3da92928e2afcf60cca2ab27eea6bd1f39a482
MD5 2e11602b531c987c9d414e8190a6dce8
BLAKE2b-256 3a0db8d8803f97c686af0c118b5b13a66c5c65ffd346b35fc20cd0d633ec74ac

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.5.5 This release

2 files

0.4.26

2 files

0.4.24

2 files

0.4.23

2 files

0.4.19

2 files

0.4.18

2 files

0.4.17

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 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