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

initial jinja rendering in Python #34

Merged
merged 10 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/type-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: prefix-dev/setup-pixi@v0.5.1
with:
pixi-version: "latest"
environments: lint
environments: type-checking

- name: type check
run: |
Expand Down
2,958 changes: 1,016 additions & 1,942 deletions pixi.lock

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ conda = ">=4.2"
pygithub = ">=2,<3"
tomli = ">=2.0.1,<3"
typing-extensions = ">=4.12.2,<4.13"
jinja2 = ">=3.0.2,<4"
types-PyYAML = ">=6.0.12.20240311,<6.0.13"

[pypi-dependencies]
rattler-build-conda-compat = { path = ".", editable = true }
Expand All @@ -28,7 +30,7 @@ pytest = ">=8.2.2,<9"
syrupy = ">=4.6.1,<5"

[feature.tests.tasks]
tests = "pytest tests"
tests = "pytest --doctest-modules"
snapshot_update = "pytest --snapshot-update tests"

[feature.lint.dependencies]
Expand All @@ -42,6 +44,11 @@ ruff = ">=0.5.0,<0.6"
[feature.lint.tasks]
pre-commit-install = "pre-commit-install"
pre-commit-run = "pre-commit run"

[feature.type-checking.dependencies]
mypy = ">=1.10.1,<2"

[feature.type-checking.tasks]
type-check = "mypy src"

[feature.py312.dependencies]
Expand All @@ -66,3 +73,4 @@ py310 = ["py310", "tests"]
py39 = ["py39", "tests"]
py38 = ["py38", "tests"]
lint = { features = ["lint"], no-default-feature = true }
type-checking = { features = ["type-checking"] }
80 changes: 80 additions & 0 deletions src/rattler_build_conda_compat/jinja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from __future__ import annotations

from typing import Any, TypedDict

import jinja2
import yaml
from jinja2 import DebugUndefined

from rattler_build_conda_compat.loader import load_yaml


class RecipeWithContext(TypedDict, total=False):
context: dict[str, str]


class _MissingUndefined(DebugUndefined):
def __str__(self) -> str:
"""
By default, `DebugUndefined` return values in the form `{{ value }}`.
`rattler-build` has a different syntax, so we need to override this method,
and return the value in the form `${{ value }}`.
"""
return f"${super().__str__()}"


def jinja_env() -> jinja2.Environment:
"""
Create a `rattler-build` specific Jinja2 environment with modified syntax.
"""
return jinja2.Environment(
variable_start_string="${{",
variable_end_string="}}",
trim_blocks=True,
lstrip_blocks=True,
autoescape=True,
undefined=_MissingUndefined,
)


def load_recipe_context(context: dict[str, str], jinja_env: jinja2.Environment) -> dict[str, str]:
"""
Load all string values from the context dictionary as Jinja2 templates.
"""
# Process each key-value pair in the dictionary
for key, value in context.items():
# If the value is a string, render it as a template
if isinstance(value, str):
template = jinja_env.from_string(value)
rendered_value = template.render(context)
context[key] = rendered_value

return context


def render_recipe_with_context(recipe_content: RecipeWithContext) -> dict[str, Any]:
"""
Render the recipe using known values from context section.
Unknown values are not evaluated and are kept as it is.

Examples:
---
```python
>>> from pathlib import Path
>>> from rattler_build_conda_compat.loader import load_yaml
>>> recipe_content = load_yaml((Path().resolve() / "tests" / "data" / "eval_recipe_using_context.yaml").read_text())
>>> evaluated_context = render_recipe_with_context(recipe_content)
>>> assert "my_value-${{ not_present_value }}" == evaluated_context["build"]["string"]
>>>
```
"""
env = jinja_env()
context = recipe_content.get("context", {})
# load all context templates
context_templates = load_recipe_context(context, env)

# render the rest of the document with the values from the context
# and keep undefined expressions _as is_.
template = env.from_string(yaml.dump(recipe_content))
rendered_content = template.render(context_templates)
return load_yaml(rendered_content)
15 changes: 15 additions & 0 deletions tests/__snapshots__/test_jinja.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# serializer version: 1
# name: test_render_context
'''
build:
string: ${{ blas_variant }}${{ hash }}_foo-bla
context:
name: foo
name_version: foo-bla
version: bla
package:
name: foo
version: bla

'''
# ---
11 changes: 11 additions & 0 deletions tests/data/context.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
context:
name: "foo"
version: "bla"
name_version: ${{ name }}-${{ version }}

package:
name: ${{ name }}
version: ${{ version }}

build:
string: ${{ blas_variant }}${{ hash }}_${{ name_version }}
4 changes: 4 additions & 0 deletions tests/data/eval_recipe_using_context.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
context:
name: "my_value"
build:
string: ${{ name }}-${{ not_present_value }}
15 changes: 15 additions & 0 deletions tests/test_jinja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pathlib import Path

import yaml
from rattler_build_conda_compat.jinja import load_yaml, render_recipe_with_context


def test_render_recipe_with_context(snapshot) -> None:
recipe = Path("tests/data/context.yaml")
with recipe.open() as f:
recipe_yaml = load_yaml(f)

rendered = render_recipe_with_context(recipe_yaml)
into_yaml = yaml.dump(rendered)

assert into_yaml == snapshot
1 change: 1 addition & 0 deletions tests/test_rattler_render.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
from pathlib import Path
from typing import TYPE_CHECKING, Any

from rattler_build_conda_compat.loader import parse_recipe_config_file
Expand Down
Loading