diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..9a34a3e --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,5 @@ +ARG IMAGE="python:3.12" +FROM --platform=amd64 mcr.microsoft.com/devcontainers/${IMAGE} +RUN apt-get update \ + && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends build-essential libssl-dev gdb cmake diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..4563925 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Cookie Cutter - Render Engine", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + "IMAGE": "python:3.12" + } + }, + "postCreateCommand": ". .devcontainer/setup.sh", + "customizations": { + "vscode": { + "settings": { + "python.formatting.provider": "charliermarsh.ruff", + "python.testing.pytestEnabled": true, + "python.testing.pytestPath": "pytest", + "python.testing.unittestEnabled": false, + "python.editor.formatOnSave": true, + "python.editor.codeActionsOnSave": {"source.fixAll": true}, + "python.testing.pytestArgs": [ + "tests" + ], + ".markdownlint-cli2.ignores": [".gitignore"] + + }, + "extensions": [ + "ms-python.python", + "charliermarsh.ruff", + "GitHub.vscode-github-actions", + "yzhang.markdown-all-in-one", + "DavidAnson.vscode-markdownlint", + "ms-vscode.makefile-tools" + ] + } + } +} diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100644 index 0000000..159409f --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,6 @@ +python -m pip install --user --upgrade pip +python -m venv .venv +source .venv/bin/activate +python -m pip install pip-tools +python -m piptools compile --upgrade -o requirements.txt requirements.in +python -m pip install -r requirements-dev.txt diff --git a/.github/workflows/devcontainer-ci.yml b/.github/workflows/devcontainer-ci.yml new file mode 100644 index 0000000..05f4c7b --- /dev/null +++ b/.github/workflows/devcontainer-ci.yml @@ -0,0 +1,28 @@ +name: Check Dev Container + +on: + push: + branches: [ main ] + paths: + - ".devcontainer/**" + - ".github/workflows/devcontainer-ci.yaml" + pull_request: + branches: [ main ] + paths: + - ".devcontainer/**" + - ".github/workflows/devcontainer-ci.yaml" + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + - run: npm install -g @devcontainers/cli + - run: devcontainer build --config ./.devcontainer/devcontainer.json --workspace-folder "$(pwd)" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b49aba2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,28 @@ +name: PyTest +on: + workflow_call: + workflow_dispatch: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + fail-fast: true + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: python -m pip install --upgrade pip -r requirements-dev.txt + - name: Run tests + run: python tests/test_template.py diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..2593c30 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +pip-tools +pytest + +-r requirements.txt diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..c8e988b --- /dev/null +++ b/requirements.in @@ -0,0 +1 @@ +cookiecutter diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5ad0c53 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,50 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --output-file=requirements.txt requirements.in +# +arrow==1.3.0 + # via cookiecutter +binaryornot==0.4.4 + # via cookiecutter +certifi==2024.8.30 + # via requests +chardet==5.2.0 + # via binaryornot +charset-normalizer==3.4.0 + # via requests +click==8.1.7 + # via cookiecutter +cookiecutter==2.6.0 + # via -r requirements.in +idna==3.10 + # via requests +jinja2==3.1.4 + # via cookiecutter +markdown-it-py==3.0.0 + # via rich +markupsafe==3.0.2 + # via jinja2 +mdurl==0.1.2 + # via markdown-it-py +pygments==2.18.0 + # via rich +python-dateutil==2.9.0.post0 + # via arrow +python-slugify==8.0.4 + # via cookiecutter +pyyaml==6.0.2 + # via cookiecutter +requests==2.32.3 + # via cookiecutter +rich==13.9.4 + # via cookiecutter +six==1.17.0 + # via python-dateutil +text-unidecode==1.3 + # via python-slugify +types-python-dateutil==2.9.0.20241206 + # via arrow +urllib3==2.2.3 + # via requests diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..ceb893c --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +python_files = test_*.py +python_classes = [A-Z]*Test +python_functions = test_* diff --git a/tests/test_template.py b/tests/test_template.py new file mode 100644 index 0000000..33ea7ac --- /dev/null +++ b/tests/test_template.py @@ -0,0 +1,88 @@ +""" +Test the Cookiecutter template. +""" +from cookiecutter.generate import generate_context +from cookiecutter.main import cookiecutter +from pathlib import Path +from shlex import split +from subprocess import run +from venv import create + +import pytest + + +@pytest.fixture(scope="session") +def template() -> Path: + """ The template under test. + + """ + return Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="module") +def tmpdir(tmp_path_factory) -> Path: + """ Test directory. + + """ + return tmp_path_factory.mktemp("test_template") + + +@pytest.fixture(scope="module") +def context(template) -> dict: + """ Template context for testing. + + """ + context = generate_context(template.joinpath("cookiecutter.json")) + context["cookiecutter"].update({ + "project_slug": "slugify" + }) + return context["cookiecutter"] + + +@pytest.fixture(scope="module") +def project(tmpdir, template, context) -> Path: + """ Create a test project from the Cookiecutter template. + + """ + cookiecutter(str(template), no_input=True, output_dir=tmpdir, extra_context=context) + return tmpdir / context["project_slug"] + + +@pytest.fixture +def python(tmp_path): + """ Create a Python virtual environment for testing. + + """ + venv = tmp_path / ".venv" + create(venv, with_pip=True) + return venv / "bin" / "python" + + +def test_project(project): + """ Verify that the project was created correctly. + + """ + # Just a basic sanity test. + assert len(list(project.iterdir())) == 4 + return + + +@pytest.fixture +def install_render_engine_cli(python): + install_cli = "pip install render_engine[cli]" + install_args = split(f"{python} -m {install_cli}") + install_process = run(install_args) + assert install_process.returncode == 0 + + +def test_site_generation(context, project, python, install_render_engine_cli): + generate_site = "render_engine build app:app" + generate_args = split(f"{python} -m {generate_site}") + generate_process = run(generate_args, cwd=project) + assert generate_process.returncode == 0 + + +# Make the script executable. + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__]))