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

chore: bump version to 1.4.1 #293

Merged
merged 11 commits into from
Nov 13, 2023
Merged
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM fedora as manpage_builder
FROM fedora:38 as manpage_builder
RUN dnf install -y make pandoc python3.11-pip
WORKDIR /app
RUN pip install poetry
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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"
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()
67 changes: 38 additions & 29 deletions docs/source/man.j2

Large diffs are not rendered by default.

67 changes: 38 additions & 29 deletions docs/source/man.rst

Large diffs are not rendered by default.

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
190 changes: 20 additions & 170 deletions poetry.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "qpc"
version = "1.4.0"
version = "1.4.1"
description = ""
authors = ["QPC Team <quipucords@redhat.com>"]
license = "GPLv3"
@@ -19,7 +19,6 @@ requests = ">=2.28.1"
cryptography = ">=37.0.4"
packaging = "^23.1"
setuptools = "^67.8.0"
gitpython = "^3.1.32"

[tool.poetry.group.dev.dependencies]
coverage = ">=6.4.2"
@@ -30,12 +29,13 @@ pytest-lazy-fixture = ">=0.6.3"
requests-mock = ">=1.9.3"
pytest-mock = "^3.8.2"
rstcheck = "^6.1.1"
ruff = "^0.0.292"
ruff = "^0.1.3"
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"]
26 changes: 21 additions & 5 deletions qpc/release.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""File to hold release constants."""
import os
import subprocess
from pathlib import Path

import git

from . import __package__version__

VERSION = __package__version__
@@ -20,7 +19,24 @@ def get_current_sha1() -> str:
return qpc_commit
try:
repo_root = Path(__file__).absolute().parent.parent
repo = git.Repo(repo_root)
except git.exc.InvalidGitRepositoryError:
git_env = os.environ.copy()
git_env["LANG"] = "C"
git_env["LC_ALL"] = "C"
git_result = subprocess.run(
("git", "rev-parse", "HEAD"),
env=git_env,
cwd=repo_root,
capture_output=True,
check=True,
)
except (FileNotFoundError, subprocess.CalledProcessError):
# FileNotFoundError raises when `git` program not found.
# CalledProcessError raises when `git` has non-zero return code.
return "UNKNOWN"
git_sha1 = git_result.stdout.decode().split("\n")[0]
try:
int(git_sha1, 16)
except ValueError:
# ValueError raises when the string does not contain a hexadecimal value.
return "UNKNOWN"
return repo.rev_parse("HEAD").hexsha
return git_sha1
4 changes: 2 additions & 2 deletions qpc/source/__init__.py
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
OPENSHIFT_SOURCE_TYPE = "openshift"
SATELLITE_SOURCE_TYPE = "satellite"
VCENTER_SOURCE_TYPE = "vcenter"
ACS_SOURCE_TYPE = "acs"
RHACS_SOURCE_TYPE = "rhacs"

SOURCE_URI = "/api/v1/sources/"
SOURCE_TYPE_CHOICES = [
@@ -21,7 +21,7 @@
OPENSHIFT_SOURCE_TYPE,
SATELLITE_SOURCE_TYPE,
VCENTER_SOURCE_TYPE,
ACS_SOURCE_TYPE,
RHACS_SOURCE_TYPE,
]

BOOLEAN_CHOICES = ["true", "false"]
35 changes: 22 additions & 13 deletions qpc/test_release.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test the `qpc.release` module."""
import subprocess
from unittest import mock

from qpc import release
@@ -12,23 +13,31 @@ def test_get_current_sha1_uses_env_var():
assert actual_value == expected_value, "failed to get value from environment"


def test_get_current_sha1_uses_git():
"""Test getting the actual current from git."""
@mock.patch("qpc.release.subprocess.run")
@mock.patch.dict(release.os.environ, {"QPC_COMMIT": ""})
def test_get_current_sha1_uses_git(mock_run):
"""Test getting the actual current commit from git."""
expected_value = "DECAFBAD"
with mock.patch.dict(release.os.environ, {"QPC_COMMIT": ""}), mock.patch(
"git.Repo.rev_parse"
) as mock_rev_parse:
mock_rev_parse.return_value.hexsha = expected_value
actual_value = release.get_current_sha1()
mock_run.return_value.stdout = expected_value.encode()
actual_value = release.get_current_sha1()
assert actual_value == expected_value, "failed to get SHA-1 value from git"


def test_get_current_sha1_unknown():
@mock.patch("qpc.release.subprocess.run")
@mock.patch.dict(release.os.environ, {"QPC_COMMIT": ""})
def test_get_current_sha1_unknown_no_git_repo(mock_run):
"""Test trying to get the SHA-1 when the env var and git repo are both missing."""
expected_value = "UNKNOWN"
with mock.patch.dict(release.os.environ, {"QPC_COMMIT": ""}), mock.patch(
"git.Repo"
) as mock_repo_class:
mock_repo_class.side_effect = release.git.exc.InvalidGitRepositoryError
actual_value = release.get_current_sha1()
mock_run.side_effect = subprocess.CalledProcessError(returncode=420, cmd="git")
actual_value = release.get_current_sha1()
assert actual_value == expected_value, "failed to get UNKNOWN value"


@mock.patch("qpc.release.subprocess.run")
@mock.patch.dict(release.os.environ, {"QPC_COMMIT": ""})
def test_get_current_sha1_unknown_unexpected_git_stdout(mock_run):
"""Test trying to get the SHA-1 when the git outputs a non-hexadecimal value."""
expected_value = "UNKNOWN"
mock_run.return_value.stdout = "this is not hexadecimal".encode()
actual_value = release.get_current_sha1()
assert actual_value == expected_value, "failed to get UNKNOWN value"
12 changes: 5 additions & 7 deletions requirements-build.txt
Original file line number Diff line number Diff line change
@@ -32,19 +32,18 @@ pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0"
# via cffi
semantic-version==2.10.0
# via setuptools-rust
setuptools-rust==1.7.0
setuptools-rust==1.8.1
# via cryptography
setuptools-scm==8.0.4
# via
# pluggy
# setuptools-rust
trove-classifiers==2023.9.19
# setuptools-scm
trove-classifiers==2023.10.18
# via hatchling
typing-extensions==4.8.0
# via
# setuptools-rust
# setuptools-scm
wheel==0.41.2
# via setuptools-scm
wheel==0.41.3
# via cryptography

# The following packages are considered to be unsafe in a requirements file:
@@ -53,7 +52,6 @@ setuptools==68.2.2 ; python_version >= "3.11" and python_version < "4.0"
# calver
# cffi
# cryptography
# gitpython
# pathspec
# pluggy
# setuptools-rust
3 changes: 0 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -2,12 +2,9 @@ certifi==2023.7.22 ; python_version >= "3.11" and python_version < "4.0"
cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
charset-normalizer==3.3.0 ; python_version >= "3.11" and python_version < "4.0"
cryptography==41.0.4 ; python_version >= "3.11" and python_version < "4.0"
gitdb==4.0.10 ; python_version >= "3.11" and python_version < "4.0"
gitpython==3.1.37 ; python_version >= "3.11" and python_version < "4.0"
idna==3.4 ; python_version >= "3.11" and python_version < "4.0"
packaging==23.2 ; python_version >= "3.11" and python_version < "4.0"
pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0"
requests==2.31.0 ; python_version >= "3.11" and python_version < "4.0"
setuptools==67.8.0 ; python_version >= "3.11" and python_version < "4.0"
smmap==5.0.1 ; python_version >= "3.11" and python_version < "4.0"
urllib3==2.0.6 ; python_version >= "3.11" and python_version < "4.0"