Skip to content

Commit

Permalink
Add pytest fixtures to be used in template development (Fix #29)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericof committed May 27, 2024
1 parent ece422a commit 08e15a2
Show file tree
Hide file tree
Showing 12 changed files with 435 additions and 1 deletion.
Empty file.
152 changes: 152 additions & 0 deletions cookieplone/templates/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import json
import re
from pathlib import Path

import pytest
from binaryornot.check import is_binary

from . import types


@pytest.fixture(scope="session")
def variable_pattern() -> re.Pattern:
return re.compile(
"({{ ?(cookiecutter)[.]([a-zA-Z0-9-_]*)|{%.+(cookiecutter)[.]([a-zA-Z0-9-_]*).+%})" # noQA: E501
)


@pytest.fixture(scope="session")
def find_variables(variable_pattern) -> types.VariableFinder:
"""Find variables in a string."""

def func(data: str) -> set:
keys = set()
matches = variable_pattern.findall(data) or []
for match in matches:
# Remove empty matches
match = [item for item in match if item.strip()]
keys.add(match[-1])
return keys

return func


def _as_sorted_list(value: set) -> list:
"""Convert a set to a list and sort it."""
value = list(value)
return sorted(value)


@pytest.fixture(scope="session")
def template_repository_root() -> Path:
"""Template root."""
return Path().cwd().resolve()


@pytest.fixture(scope="session")
def template_folder_name() -> str:
"""Name used for the template folder."""
return "{{ cookiecutter.__folder_name }}"


@pytest.fixture(scope="session")
def valid_key() -> types.VariableValidator:
"""Check if we will check for this key."""

def func(key: str, ignore: list[str] | None = None) -> bool:
ignore = ignore if ignore else ["__prompts__"]
return all([
key not in ignore,
key.startswith("__") or not key.startswith("_"),
])

return func


@pytest.fixture
def configuration_data(template_repository_root) -> dict:
"""Return configuration from cookiecutter.json."""
file_ = template_repository_root / "cookiecutter.json"
return json.loads(file_.read_text())


@pytest.fixture
def configuration_variables(configuration_data, valid_key) -> set[str]:
"""Return a set of variables available in cookiecutter.json."""
return {key for key in configuration_data if valid_key(key)}


@pytest.fixture
def sub_templates(configuration_data, template_repository_root) -> list[Path]:
"""Return a list of subtemplates used by this template."""
templates = []
parent = template_repository_root.parent
sub_templates = configuration_data.get("__cookieplone_subtemplates", [])
for sub_template in sub_templates:
sub_template_id = sub_template[0]
sub_template_path = (parent / sub_template_id).resolve()
if not sub_template_path.exists():
sub_template_path = (parent.parent / sub_template_id).resolve()
templates.append(sub_template_path)
return templates


def _all_files_in_template(
base_path: Path, template_folder_name_: str, include_configuration: bool = True
) -> list[Path]:
"""Get all files in a template repository."""
hooks_folder = base_path / "hooks"
hooks_files = list(hooks_folder.glob("**/*"))
template_folder = base_path / template_folder_name_
project_files = list(template_folder.glob("**/*"))
all_files = hooks_files + project_files
if include_configuration:
all_files.append(base_path / "cookiecutter.json")
return all_files


@pytest.fixture
def project_variables(
template_repository_root,
find_variables,
configuration_data,
template_folder_name,
sub_templates,
) -> set[str]:
"""Return a set with all variables used in the project."""
base_data = f"{json.dumps(configuration_data)} {template_folder_name}"
variables = find_variables(base_data)
all_files = _all_files_in_template(template_repository_root, template_folder_name)
for sub_template_path in sub_templates:
all_files.extend(
_all_files_in_template(sub_template_path, template_folder_name, True)
)
for filepath in all_files:
data = filepath.name
is_file = filepath.is_file()
if is_file and not is_binary(f"{filepath}"):
data = f"{data} {filepath.read_text()}"
variables.update(find_variables(data))
return variables


@pytest.fixture
def variables_required() -> set[str]:
"""Variables required to be present, even if not used."""
return {"__cookieplone_repository_path", "__cookieplone_template"}


@pytest.fixture
def variables_missing(configuration_variables, project_variables) -> list[str]:
"""Return a list of variables used in the project but not in the configuration."""
return _as_sorted_list(project_variables.difference(configuration_variables))


@pytest.fixture
def variables_not_used(
configuration_variables, project_variables, variables_required
) -> list[str]:
"""Return a list of variables in the configuration but not used in the project."""
# Add variables_required
project_variables.update(variables_required)
return _as_sorted_list(configuration_variables.difference(project_variables))
13 changes: 13 additions & 0 deletions cookieplone/templates/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pathlib import Path
from typing import Protocol

StrPath = str | Path
DataStructure = dict | list


class VariableFinder(Protocol):
def __call__(self, data: str) -> set: ...


class VariableValidator(Protocol):
def __call__(self, key: str, ignore: list[str] | None) -> bool: ...
1 change: 1 addition & 0 deletions news/29.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add pytest fixtures to be used in template development [@ericof]
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ dependencies = [
"packaging==24.0",
"gitpython==3.1.43",
"xmltodict==0.13.0",
"black",
"black==24.4.2",
"isort",
"zpretty"
]

[project.entry-points.pytest11]
cookieplone = "cookieplone.templates.fixtures"

[project.scripts]
cookieplone = 'cookieplone.__main__:main'

Expand Down
26 changes: 26 additions & 0 deletions tests/_resources/templates/project/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"title": "Addon",
"description": "A new addon for Plone",
"check": "1",
"__folder_name": "test",
"__profile_language": "en",
"__prompts__": {
"title": "Addon Title",
"description": "A short description of your addon"
},
"_copy_without_render": [],
"_extensions": [
"cookieplone.filters.pascal_case",
"cookieplone.filters.package_name",
"cookieplone.filters.package_namespace"
],
"__cookieplone_subtemplates": [
[
"sub/bar",
"Bar",
"1"
]
],
"__cookieplone_repository_path": "",
"__cookieplone_template": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# {{ cookiecutter.title }}
## {{ cookiecutter.description }}

{%- if cookiecutter.check == '1' %}
{{ cookiecutter.__profile_language}}
{%- endif %}
20 changes: 20 additions & 0 deletions tests/_resources/templates/sub/bar/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"title": "Addon",
"description": "A new addon for Plone",
"check": "1",
"double_check": "1",
"__folder_name": "test",
"__profile_language": "en",
"__prompts__": {
"title": "Addon Title",
"description": "A short description of your addon"
},
"_copy_without_render": [],
"_extensions": [
"cookieplone.filters.pascal_case",
"cookieplone.filters.package_name",
"cookieplone.filters.package_namespace"
],
"__cookieplone_repository_path": "",
"__cookieplone_template": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# {{ cookiecutter.title }}
## {{ cookiecutter.description }}

BAR!

{%- if cookiecutter.check == '1' and cookiecutter.double_check == '1' %}
{{ cookiecutter.__profile_language}}
{%- endif %}
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import pytest
from git import Repo

pytest_plugins = "pytester"


@pytest.fixture()
def tmp_repo(tmp_path):
Expand Down Expand Up @@ -34,3 +36,9 @@ def func(filepath: str) -> str:
return data

return func


@pytest.fixture(scope="session")
def project_source() -> Path:
path = (Path(__file__).parent / "_resources" / "templates").resolve()
return path
31 changes: 31 additions & 0 deletions tests/templates/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import shutil

import pytest


@pytest.fixture
def project_folder(pytester, project_source):
"""Create fake cookiecutter project."""
dest_path = pytester._path / "templates"
if dest_path.exists():
shutil.rmtree(dest_path, True)
shutil.copytree(project_source, dest_path)
return dest_path


@pytest.fixture
def testdir(pytester, project_folder):
# create a temporary conftest.py file
pytester.makeconftest(
f"""
from pathlib import Path
import pytest
pytest_plugins = ["cookieplone.templates.fixtures"]
@pytest.fixture(scope="session")
def template_repository_root() -> Path:
return Path("{project_folder}") / "project"
"""
)
return pytester
Loading

0 comments on commit 08e15a2

Please sign in to comment.