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

replace jinja-cli with local python script #285

Merged
merged 4 commits into from
Oct 27, 2023
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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ generate-man:
@export QPC_VAR_CURRENT_YEAR=$(shell date +'%Y') \
&& export QPC_VAR_PROJECT=$${QPC_VAR_PROJECT:-Quipucords} \
&& export QPC_VAR_PROGRAM_NAME=$${QPC_VAR_PROGRAM_NAME:-qpc} \
&& poetry run jinja -X QPC_VAR docs/source/man.j2 $(ARGS)
&& poetry run python docs/jinja-render.py -e '^QPC_VAR.*' -t docs/source/man.j2 $(ARGS)

update-man.rst:
$(MAKE) generate-man ARGS="-o docs/source/man.rst"
Expand Down
78 changes: 78 additions & 0 deletions docs/jinja-render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
Barebones command-line utility to render a Jinja template.

Uses environment variables to populate the template.

Example usage:

# define relevant environment variables
export QPC_VAR_PROGRAM_NAME=qpc
export QPC_VAR_PROJECT=Quipucords
export QPC_VAR_CURRENT_YEAR=$(date +'%Y')

# use stdin to read template and stdout to write output:
python ./jinja-render.py -e '^QPC_VAR_.*' \
< ./source/man.j2 > ./source/man.rst

# use arguments to specify template and output paths:
python ./jinja-render.py -e '^QPC_VAR_.*' \
-t ./source/man.j2 -o ./source/man.rst
"""

import argparse
import os
import re

from jinja2 import DictLoader, Environment


def get_env_vars(allow_pattern):
"""Get the matching environment variables."""
env_vars = {}
re_pattern = re.compile(allow_pattern)
for key, value in os.environ.items():
if re_pattern.search(key):
env_vars[key] = value
return env_vars


def get_template(template_file):
"""Load the Jinja template."""
with template_file as f:
template_data = f.read()
return Environment(
loader=DictLoader({"-": template_data}), keep_trailing_newline=True
).get_template("-")


def get_args():
"""Parse and return command line arguments."""
parser = argparse.ArgumentParser(description="Format Jinja template using env vars")
parser.add_argument(
"-e",
"--env_var_pattern",
type=str,
default="",
help="regex pattern to match environment variable names",
)
parser.add_argument("-o", "--output", type=argparse.FileType("w"), default="-")
parser.add_argument("-t", "--template", type=argparse.FileType("r"), default="-")
args = parser.parse_args()
return args


def main():
"""Parse command line args and render Jinja template to output."""
args = get_args()
template = get_template(template_file=args.template)
env_vars = get_env_vars(allow_pattern=args.env_var_pattern)
args.output.write(template.render(env_vars))
if hasattr(args.output, "name"):
# This is a side effect of how ArgumentParser handles files vs stdout.
# Real output files have a "name" attribute and should be closed.
# However, we do NOT want to close if it's stdout, which has no name.
args.output.close()


if __name__ == "__main__":
main()
101 changes: 101 additions & 0 deletions docs/test_jina_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Tests for jinja-render.py standalone script."""

import importlib.util
import os
import random
import sys
import tempfile
from io import StringIO
from pathlib import Path

import pytest

sample_jinja_template = "hello, {{ NAME }}"


@pytest.fixture(scope="module")
def jinja_render():
"""
Import the jinja-render script as a module.

This is necessary because jinja-render.py is a standalone script
that does not live in a regular Python package.
"""
module_name = "jinja_render"
file_path = Path(__file__).parent / "jinja-render.py"
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def test_get_env_vars(jinja_render, mocker):
"""Test getting only env vars that match the given pattern."""
mocker.patch.dict(
os.environ,
{
"unrelated": "zero",
"QPC_THING": "one",
"QPC_thang": "two",
"NOT_QPC_OTHER": "three",
},
clear=True,
)
expected = {"QPC_THING": "one", "QPC_thang": "two"}
allow_pattern = "^QPC_.*"
actual = jinja_render.get_env_vars(allow_pattern)
assert actual == expected


def test_read_stdin_write_stdout(jinja_render, mocker, capsys):
"""Test reading the Jinja template from stdin and writing output to stdout."""
fake_name = str(random.random())
expected_stdout = f"hello, {fake_name}"

fake_env_vars = {"NAME": fake_name}
fake_sys_argv = ["script.py", "-e", ".*"]
fake_stdin = StringIO(sample_jinja_template)

mocker.patch.dict(os.environ, fake_env_vars, clear=True)
mocker.patch.object(sys, "argv", fake_sys_argv)
mocker.patch.object(sys, "stdin", fake_stdin)

jinja_render.main()
actual_stdout = capsys.readouterr().out
assert actual_stdout == expected_stdout


@pytest.fixture
def template_path():
"""Temp file containing a Jija template."""
tmp_file = tempfile.NamedTemporaryFile()
tmp_file.write(sample_jinja_template.encode())
tmp_file.seek(0)
yield tmp_file.name
tmp_file.close()


def test_read_file_write_file(jinja_render, template_path, mocker, capsys):
"""Test reading the Jinja template from file and writing output to file."""
fake_name = str(random.random())
expected_stdout = f"hello, {fake_name}"
fake_env_vars = {"NAME": fake_name}
with tempfile.TemporaryDirectory() as output_directory:
output_path = Path(output_directory) / str(random.random())
fake_sys_argv = [
"script.py",
"-e",
".*",
"-t",
template_path,
"-o",
str(output_path),
]
mocker.patch.dict(os.environ, fake_env_vars, clear=True)
mocker.patch.object(sys, "argv", fake_sys_argv)
jinja_render.main()

with output_path.open() as output_file:
actual_output = output_file.read()

assert actual_output == expected_stdout
90 changes: 1 addition & 89 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ ruff = "^0.0.292"
pip-tools = "^7.1.0"
pybuild-deps = "^0.1.1"


[tool.poetry.group.build.dependencies]
jinja-cli = "^1.2.2"
jinja2 = "^3.1.2"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
2 changes: 1 addition & 1 deletion requirements-build.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ setuptools-scm==8.0.4
# via
# pluggy
# setuptools-rust
trove-classifiers==2023.9.19
trove-classifiers==2023.10.18
# via hatchling
typing-extensions==4.8.0
# via
Expand Down
Loading