Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jinja2 Templating Infra #446

Merged
merged 8 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion flepimop/gempyor_pkg/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies = [
"emcee",
"graphviz",
"h5py",
"Jinja2",
"matplotlib",
"numba>=0.53.1",
"numpy",
Expand Down Expand Up @@ -51,7 +52,8 @@ gempyor-simulate = "gempyor.simulate:_deprecated_simulate"

[tool.setuptools]
package-dir = {"" = "src"}
include-package-data = false
include-package-data = true
package-data = { "gempyor" = ["templates/*.j2"] }

[tool.setuptools.packages.find]
where = ["src"]
Expand Down
120 changes: 120 additions & 0 deletions flepimop/gempyor_pkg/src/gempyor/_jinja.py
TimothyWillard marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
Internal Jinja2 template rendering utilities.

This module contains utilities for finding and rendering Jinja2 templates. This module
is tightly coupled to the organization of the package and not intended for external use.
"""

# Exports
__all__ = []


# Imports
from os.path import dirname
from pathlib import Path
from tempfile import mkstemp
from typing import Any

from jinja2 import Environment, FileSystemLoader, PackageLoader, Template


# Globals
try:
_jinja_environment = Environment(
loader=FileSystemLoader(dirname(__file__).replace("\\", "/") + "/templates")
)
except ValueError:
_jinja_environment = Environment(loader=PackageLoader("gempyor"))


# Functions
def _get_template(name: str) -> Template:
"""
Get a jinja template by name.

Args:
name: The name of the template to pull.

Returns:
A jinja template object corresponding to `name`.

Examples:
>>> _get_template("test_template.j2")
<Template 'test_template.j2'>
"""
return _jinja_environment.get_template(name)


def _render_template(name: str, data: dict[str, Any]) -> str:
"""
Render a jinja template by name.

Args:
name: The name of the template to pull.
data: The data to pass to the template when rendering.

Returns:
The rendered template as a string.

Examples:
>>> _render_template("test_template.j2", {"name": "Bob"})
'Hello Bob!'
"""
return _get_template(name).render(data)


def _render_template_to_file(name: str, data: dict[str, Any], file: Path) -> None:
"""
Render a jinja template and save to a file.

Args:
name: The name of the template to pull.
data: The data to pass to the template when rendering.
file: The file to save the rendered template to.

Examples:
>>> from pathlib import Path
>>> file = Path("hi.txt")
>>> _render_template_to_file("test_template.j2", {"name": "Jane"}, file)
>>> file.read_text()
'Hello Jane!'
"""
with file.open(mode="w", encoding="utf-8") as f:
f.write(_render_template(name, data))


def _render_template_to_temp_file(
name: str,
data: dict[str, Any],
suffix: str | None = None,
prefix: str | None = None,
) -> Path:
"""
Render a jinja template and save to a temporary file.

Args:
name: The name of the template to pull.
data: The data to pass to the template when rendering.
suffix: The suffix of the temporary file, such as an extension. Passed on to
`tempfile.mkstemp`.
prefix: The prefix of the temporary file. Passed on to `tempfile.mkstemp`.

Returns:
The file containing the rendered template as a `Path` object.

See Also:
[`tempfile.mkstemp`](https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp)

Examples:
>>> file = _render_template_to_temp_file(
... "test_template.j2", {"name": "John"}, suffix=".txt", prefix="foo_"
... )
>>> file
PosixPath('/var/folders/2z/h3pc0p7s3ng1tvxrgsw5kr680000gp/T/foo_ocaomg4k.txt')
>>> file.read_text()
'Hello John!'
"""
_, tmp = mkstemp(suffix=suffix, prefix=prefix, text=True)
tmp = Path(tmp)
_render_template_to_file(name, data, tmp)
return tmp
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello {{ name }}!
32 changes: 32 additions & 0 deletions flepimop/gempyor_pkg/tests/_jinja/test__get_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Any

from jinja2 import Template
from jinja2.exceptions import TemplateNotFound
import pytest

from gempyor._jinja import _get_template


@pytest.mark.parametrize(
"name", (("template_does_not_exist.j2"), ("template_does_not_exist_again.j2"))
)
def test__get_template_template_not_found_error(name: str) -> None:
with pytest.raises(TemplateNotFound, match=name):
_get_template(name)


@pytest.mark.parametrize(
("name", "data", "output"),
(
("test_template.j2", {"name": "John"}, "Hello John!"),
("test_template.j2", {"name": "Jake"}, "Hello Jake!"),
("test_template.j2", {"name": "Jane"}, "Hello Jane!"),
),
)
def test__get_template_renders_correctly(
name: str, data: dict[str, Any], output: str
) -> None:
template = _get_template(name)
assert isinstance(template, Template)
rendered_template = template.render(**data)
assert rendered_template == output
19 changes: 19 additions & 0 deletions flepimop/gempyor_pkg/tests/_jinja/test__render_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Any

import pytest

from gempyor._jinja import _render_template


@pytest.mark.parametrize(
("name", "data", "output"),
(
("test_template.j2", {"name": "John"}, "Hello John!"),
("test_template.j2", {"name": "Jake"}, "Hello Jake!"),
("test_template.j2", {"name": "Jane"}, "Hello Jane!"),
),
)
def test__render_template_renders_correctly(
name: str, data: dict[str, Any], output: str
) -> None:
assert _render_template(name, data) == output
24 changes: 24 additions & 0 deletions flepimop/gempyor_pkg/tests/_jinja/test__render_template_to_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pathlib import Path
from typing import Any

import pytest

from gempyor._jinja import _render_template_to_file


@pytest.mark.parametrize(
("name", "data", "file", "output"),
(
("test_template.j2", {"name": "John"}, Path("foo.txt"), "Hello John!"),
("test_template.j2", {"name": "Jake"}, Path("bar.txt"), "Hello Jake!"),
("test_template.j2", {"name": "Jane"}, Path("fizz.md"), "Hello Jane!"),
),
)
def test__render_template_to_file_renders_correctly(
tmp_path: Path, name: str, data: dict[str, Any], file: Path, output: str
) -> None:
if file.is_absolute():
raise ValueError("The `file` argument must be relative for unit testing.")
file = tmp_path / file
_render_template_to_file(name, data, file)
assert file.read_text() == output
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Any

import pytest

from gempyor._jinja import _render_template_to_temp_file


@pytest.mark.parametrize(
("name", "data", "suffix", "prefix", "output"),
(
("test_template.j2", {"name": "John"}, None, None, "Hello John!"),
("test_template.j2", {"name": "Jake"}, ".txt", None, "Hello Jake!"),
("test_template.j2", {"name": "Jane"}, None, "abc_", "Hello Jane!"),
("test_template.j2", {"name": "Job"}, ".dat", "def_", "Hello Job!"),
),
)
def test__render_template_to_temp_file_renders_correctly(
name: str, data: dict[str, Any], suffix: str | None, prefix: str | None, output: str
) -> None:
temp_file = _render_template_to_temp_file(name, data, suffix=suffix, prefix=prefix)
assert temp_file.exists()
if suffix:
assert temp_file.name.endswith(suffix)
if prefix:
assert temp_file.name.startswith(prefix)
assert temp_file.read_text(encoding="utf-8") == output
Loading