From 3bc46877c80a1cb9253d6a7de7969c978262c23a Mon Sep 17 00:00:00 2001 From: Melissa DeLucchi Date: Thu, 9 Oct 2025 08:13:12 -0400 Subject: [PATCH 1/7] Appropriate formatting for __init__ file. REALLY run the pre-commits in the unit tests. --- .../.pre-commit-config.yaml.jinja | 2 +- .../src/{{package_name}}/__init__.py.jinja | 8 +- tests/test_package_creation.py | 100 +++--------------- 3 files changed, 23 insertions(+), 87 deletions(-) diff --git a/python-project-template/.pre-commit-config.yaml.jinja b/python-project-template/.pre-commit-config.yaml.jinja index 19ed2a7..50e6dfe 100644 --- a/python-project-template/.pre-commit-config.yaml.jinja +++ b/python-project-template/.pre-commit-config.yaml.jinja @@ -139,7 +139,7 @@ repos: {%- endif %} ] {%- endif %} -{%- if include_notebooks %} +{%- if include_docs and include_notebooks %} # Make sure Sphinx can build the documentation while explicitly omitting # notebooks from the docs, so users don't have to wait through the execution # of each notebook or each commit. By default, these will be checked in the diff --git a/python-project-template/src/{{package_name}}/__init__.py.jinja b/python-project-template/src/{{package_name}}/__init__.py.jinja index 4e745eb..f17127b 100644 --- a/python-project-template/src/{{package_name}}/__init__.py.jinja +++ b/python-project-template/src/{{package_name}}/__init__.py.jinja @@ -1,6 +1,10 @@ -from ._version import __version__ {% if create_example_module -%} +from ._version import __version__ from .example_module import greetings, meaning -__all__ = ["greetings", "meaning"] +__all__ = ["greetings", "meaning", "__version__"] +{% else -%} +from ._version import __version__ + +__all__ = ["__version__"] {% endif -%} diff --git a/tests/test_package_creation.py b/tests/test_package_creation.py index e04c134..189af10 100644 --- a/tests/test_package_creation.py +++ b/tests/test_package_creation.py @@ -1,9 +1,12 @@ """Verify package creation using `pytest-copie`""" +import os import subprocess import pytest +os.environ["SKIP"] = "no-commit-to-branch,check-added-large-files" + def create_project_with_basic_checks(copie, extra_answers, package_name="example_package"): """Create the project using copier. Perform a handful of basic checks on the created directory.""" @@ -24,12 +27,6 @@ def create_project_with_basic_checks(copie, extra_answers, package_name="example ) assert build_results.returncode == 0 - # pyproject_toml_is_valid - precommit_results = subprocess.run( - ["pre-commit", "run", "validate-pyproject"], cwd=result.project_dir, check=False - ) - assert precommit_results.returncode == 0 - # directory_structure_is_correct assert (result.project_dir / f"src/{package_name}").is_dir() and ( result.project_dir / f"tests/{package_name}" @@ -54,81 +51,17 @@ def create_project_with_basic_checks(copie, extra_answers, package_name="example print("Required file not generated:", file) assert all_found - # black_runs_successfully for src and tests - black_results = subprocess.run( - ["python", "-m", "black", "--check", "--verbose", result.project_dir], - cwd=result.project_dir, - check=False, - ) - assert black_results.returncode == 0 - - return result - - -def pylint_runs_successfully(result): - """Test to ensure that the pylint linter runs successfully on the project""" - # run pylint to ensure that the hydrated files are linted correctly - pylint_src_results = subprocess.run( - [ - "python", - "-m", - "pylint", - "--recursive=y", - "--rcfile=./src/.pylintrc", - (result.project_dir / "src"), - ], - cwd=result.project_dir, - check=False, - ) - - pylint_test_results = subprocess.run( - [ - "python", - "-m", - "pylint", - "--recursive=y", - "--rcfile=./tests/.pylintrc", - (result.project_dir / "tests"), - ], - cwd=result.project_dir, - check=False, - ) - - return pylint_src_results.returncode == 0 and pylint_test_results.returncode == 0 - - -def docs_build_successfully(result): - """Test that we can build the doc tree. - - !!! NOTE - This doesn't currently work because we need to `pip install` the hydrated - project before running the tests. And we don't have a way to create a temporary - virtual environment for the project. - """ - - required_files = [ - ".readthedocs.yml", - ] - all_found = True - for file in required_files: - if not (result.project_dir / file).is_file(): - all_found = False - print("Required file not generated:", file) - return all_found - - # sphinx_results = subprocess.run( - # ["make", "html"], - # cwd=(result.project_dir / "docs"), - # ) - - # return sphinx_results.returncode == 0 + ## Initialize local git repository and add ALL new files to it. + git_results = subprocess.run(["git", "init", "."], cwd=result.project_dir, check=False) + assert git_results.returncode == 0 + git_results = subprocess.run(["git", "add", "."], cwd=result.project_dir, check=False) + assert git_results.returncode == 0 + ## This will run ALL of the relevant pre-commits (excludes only "no-commit-to-branch,check-added-large-files") + precommit_results = subprocess.run(["pre-commit", "run", "-a"], cwd=result.project_dir, check=False) + assert precommit_results.returncode == 0 -def github_workflows_are_valid(result): - """Test to ensure that the GitHub workflows are valid""" - workflows_results = subprocess.run( - ["pre-commit", "run", "check-github-workflows"], cwd=result.project_dir, check=False - ) - return workflows_results.returncode == 0 + return result def test_all_defaults(copie): @@ -139,7 +72,8 @@ def test_all_defaults(copie): result = create_project_with_basic_checks(copie, {}) # uses ruff instead of (black/isort/pylint) - assert not pylint_runs_successfully(result) + assert not (result.project_dir / "src/.pylintrc").is_file() + assert not (result.project_dir / "tests/.pylintrc").is_file() # check to see if the README file was hydrated with copier answers. found_line = False @@ -163,7 +97,8 @@ def test_use_black_and_no_example_modules(copie): "create_example_module": False, } result = create_project_with_basic_checks(copie, extra_answers) - assert pylint_runs_successfully(result) + assert (result.project_dir / "src/.pylintrc").is_file() + assert (result.project_dir / "tests/.pylintrc").is_file() # make sure that the files that were not requested were not created assert not (result.project_dir / "src/example_package/example_module.py").is_file() @@ -263,7 +198,6 @@ def test_doc_combinations(copie, doc_answers): # run copier to hydrate a temporary project result = create_project_with_basic_checks(copie, doc_answers) - assert docs_build_successfully(result) assert (result.project_dir / "docs").is_dir() @@ -310,5 +244,3 @@ def test_github_workflows_schema(copie): "include_docs": True, } result = create_project_with_basic_checks(copie, extra_answers) - - assert github_workflows_are_valid(result) From 27d468302721aac2a6fe4d692055f039e28f0921 Mon Sep 17 00:00:00 2001 From: Melissa DeLucchi Date: Thu, 9 Oct 2025 08:32:40 -0400 Subject: [PATCH 2/7] Add dependencies for CI testing. --- pyproject.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e521286..213bfaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,20 @@ dev = [ "pylint", # test pylint in unit tests "pytest-copie", # Used to create hydrated copier projects for testing "tox", # Used to run tests in multiple environments + ## Dependencies that are needed to test hydrated projects: + "ipykernel", + "ipython", + "jupyter", + "jupytext", + "mypy", + "nbconvert", + "nbsphinx", + "pre-commit", + "ruff", + "sphinx", + "sphinx-autoapi", + "sphinx-copybutton", + "sphinx-rtd-theme", ] [build-system] From ce2280945fdab98538058b58e8dfacc6a5dfaea3 Mon Sep 17 00:00:00 2001 From: Melissa DeLucchi Date: Thu, 9 Oct 2025 08:56:48 -0400 Subject: [PATCH 3/7] Limit python versions. --- tests/conftest.py | 17 ++++++++++ tests/test_package_creation.py | 60 ++++++++++++++-------------------- 2 files changed, 42 insertions(+), 35 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..95d7545 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,17 @@ +import pytest + +PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13"] + + +def pytest_addoption(parser): + parser.addoption("--python_version", action="store", default="3.12", choices=PYTHON_VERSIONS) + + +@pytest.fixture(scope="session", name="python_version") +def python_version(request): + yield request.config.getoption("--python_version") + + +@pytest.fixture +def default_answers(python_version): + return {"python_versions": [python_version]} diff --git a/tests/test_package_creation.py b/tests/test_package_creation.py index 189af10..88ddc2c 100644 --- a/tests/test_package_creation.py +++ b/tests/test_package_creation.py @@ -52,6 +52,10 @@ def create_project_with_basic_checks(copie, extra_answers, package_name="example assert all_found ## Initialize local git repository and add ALL new files to it. + git_results = subprocess.run( + ["git", "config", "--global", "init.defaultBranch", "main"], cwd=result.project_dir, check=False + ) + assert git_results.returncode == 0 git_results = subprocess.run(["git", "init", "."], cwd=result.project_dir, check=False) assert git_results.returncode == 0 git_results = subprocess.run(["git", "add", "."], cwd=result.project_dir, check=False) @@ -64,12 +68,11 @@ def create_project_with_basic_checks(copie, extra_answers, package_name="example return result -def test_all_defaults(copie): +def test_all_defaults(copie, default_answers): """Test that the default values are used when no arguments are given. Ensure that the project is created and that the basic files exist. """ - # run copier to hydrate a temporary project - result = create_project_with_basic_checks(copie, {}) + result = create_project_with_basic_checks(copie, default_answers) # uses ruff instead of (black/isort/pylint) assert not (result.project_dir / "src/.pylintrc").is_file() @@ -85,18 +88,17 @@ def test_all_defaults(copie): assert found_line -def test_use_black_and_no_example_modules(copie): +def test_use_black_and_no_example_modules(copie, default_answers): """We want to provide non-default arguments for the linter and example modules copier questions and ensure that the pyproject.toml file is created with Black and that no example modules are created. """ - - # provide a dictionary of the non-default answers to use - extra_answers = { + extra_answers = default_answers | { "enforce_style": ["black", "pylint", "isort"], "create_example_module": False, } result = create_project_with_basic_checks(copie, extra_answers) + assert (result.project_dir / "src/.pylintrc").is_file() assert (result.project_dir / "tests/.pylintrc").is_file() @@ -126,12 +128,10 @@ def test_use_black_and_no_example_modules(copie): ["black", "pylint", "isort", "ruff_lint", "ruff_format"], ], ) -def test_code_style_combinations(copie, enforce_style): +def test_code_style_combinations(copie, enforce_style, default_answers): """Test that various combinations of code style enforcement will still result in a valid project being created.""" - - # provide a dictionary of the non-default answers to use - extra_answers = { + extra_answers = default_answers | { "enforce_style": enforce_style, } result = create_project_with_basic_checks(copie, extra_answers) @@ -146,16 +146,13 @@ def test_code_style_combinations(copie, enforce_style): ["email", "slack"], ], ) -def test_smoke_test_notification(copie, notification): +def test_smoke_test_notification(copie, notification, default_answers): """Confirm we can generate a "smoke_test.yaml" file, with all notification mechanisms selected.""" - - # provide a dictionary of the non-default answers to use - extra_answers = { + extra_answers = default_answers | { "failure_notification": notification, } - # run copier to hydrate a temporary project result = create_project_with_basic_checks(copie, extra_answers) @@ -169,13 +166,10 @@ def test_smoke_test_notification(copie, notification): ["none"], ], ) -def test_license(copie, license): +def test_license(copie, license, default_answers): """Confirm we get a valid project for different license options.""" + extra_answers = default_answers | {"license": license} - # provide a dictionary of the non-default answers to use - extra_answers = {"license": license} - - # run copier to hydrate a temporary project result = create_project_with_basic_checks(copie, extra_answers) @@ -192,11 +186,10 @@ def test_license(copie, license): }, ], ) -def test_doc_combinations(copie, doc_answers): +def test_doc_combinations(copie, doc_answers, default_answers): """Confirm the docs directory is well-formed, when including docs.""" - - # run copier to hydrate a temporary project - result = create_project_with_basic_checks(copie, doc_answers) + extra_answers = default_answers | doc_answers + result = create_project_with_basic_checks(copie, extra_answers) assert (result.project_dir / "docs").is_dir() @@ -214,32 +207,29 @@ def test_doc_combinations(copie, doc_answers): }, ], ) -def test_doc_combinations_no_docs(copie, doc_answers): +def test_doc_combinations_no_docs(copie, doc_answers, default_answers): """Confirm there is no 'docs' directory, if not including docs.""" + extra_answers = default_answers | doc_answers - # run copier to hydrate a temporary project - result = create_project_with_basic_checks(copie, doc_answers) + result = create_project_with_basic_checks(copie, extra_answers) assert not (result.project_dir / "docs").is_dir() @pytest.mark.parametrize("test_lowest_version", ["none", "direct", "all"]) -def test_test_lowest_version(copie, test_lowest_version): +def test_test_lowest_version(copie, test_lowest_version, default_answers): """Confirm we can generate a "testing_and_coverage.yaml" file, with all test_lowest_version mechanisms selected.""" - - # provide a dictionary of the non-default answers to use - extra_answers = { + extra_answers = default_answers | { "test_lowest_version": test_lowest_version, } - # run copier to hydrate a temporary project result = create_project_with_basic_checks(copie, extra_answers) -def test_github_workflows_schema(copie): +def test_github_workflows_schema(copie, default_answers): """Confirm the current GitHub workflows have valid schemas.""" - extra_answers = { + extra_answers = default_answers | { "include_benchmarks": True, "include_docs": True, } From 2d7f30ff3a0d5eb23db0cddd1743dbbce92dc691 Mon Sep 17 00:00:00 2001 From: Melissa DeLucchi Date: Thu, 9 Oct 2025 09:02:58 -0400 Subject: [PATCH 4/7] Pass CI python version --- .github/workflows/ci.yml | 4 ++-- .github/workflows/smoke-test.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdc0a07..0f9b304 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@main @@ -27,4 +27,4 @@ jobs: pip install -e .[dev] - name: Run unit tests with pytest / pytest-copie run: | - python -m pytest \ No newline at end of file + python -m pytest --python_version ${{ matrix.python-version }} \ No newline at end of file diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index f0d0791..8f3f5dd 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@main @@ -38,7 +38,7 @@ jobs: pip list - name: Run unit tests with pytest / pytest-copie run: | - python -m pytest + python -m pytest --python_version ${{ matrix.python-version }} - name: Send status to Slack app if: ${{ failure() && github.event_name != 'workflow_dispatch' }} id: slack From 5ff8bb4566a9d001d7fa8dab25c240c0742f3368 Mon Sep 17 00:00:00 2001 From: Melissa DeLucchi Date: Thu, 9 Oct 2025 09:15:14 -0400 Subject: [PATCH 5/7] Use a python range. --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 95d7545..55567cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,4 +14,5 @@ def python_version(request): @pytest.fixture def default_answers(python_version): - return {"python_versions": [python_version]} + highest_version_index = PYTHON_VERSIONS.index(python_version) + return {"python_versions": PYTHON_VERSIONS[0 : (highest_version_index + 1)]} From e038ed5f979f99bd5d48db4b471ca6910d4ae5f4 Mon Sep 17 00:00:00 2001 From: Melissa DeLucchi Date: Thu, 9 Oct 2025 09:26:25 -0400 Subject: [PATCH 6/7] Give up on black on 3.13. --- .github/workflows/ci.yml | 2 +- .github/workflows/smoke-test.yml | 2 +- tests/conftest.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f9b304..3293088 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@main diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 8f3f5dd..e67c277 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@main diff --git a/tests/conftest.py b/tests/conftest.py index 55567cc..95d7545 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,5 +14,4 @@ def python_version(request): @pytest.fixture def default_answers(python_version): - highest_version_index = PYTHON_VERSIONS.index(python_version) - return {"python_versions": PYTHON_VERSIONS[0 : (highest_version_index + 1)]} + return {"python_versions": [python_version]} From e6d2eef74eff5e3a1a08a86749786c5566876d27 Mon Sep 17 00:00:00 2001 From: Melissa DeLucchi Date: Thu, 9 Oct 2025 09:56:02 -0400 Subject: [PATCH 7/7] Better CI cleanliness --- .github/workflows/ci.yml | 1 + .github/workflows/smoke-test.yml | 1 + tests/test_package_creation.py | 6 +----- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3293088..b863e07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,4 +27,5 @@ jobs: pip install -e .[dev] - name: Run unit tests with pytest / pytest-copie run: | + git config --global init.defaultBranch main python -m pytest --python_version ${{ matrix.python-version }} \ No newline at end of file diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index e67c277..43d4098 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -38,6 +38,7 @@ jobs: pip list - name: Run unit tests with pytest / pytest-copie run: | + git config --global init.defaultBranch main python -m pytest --python_version ${{ matrix.python-version }} - name: Send status to Slack app if: ${{ failure() && github.event_name != 'workflow_dispatch' }} diff --git a/tests/test_package_creation.py b/tests/test_package_creation.py index 88ddc2c..e19e104 100644 --- a/tests/test_package_creation.py +++ b/tests/test_package_creation.py @@ -5,7 +5,7 @@ import pytest -os.environ["SKIP"] = "no-commit-to-branch,check-added-large-files" +os.environ["SKIP"] = "no-commit-to-branch" def create_project_with_basic_checks(copie, extra_answers, package_name="example_package"): @@ -52,10 +52,6 @@ def create_project_with_basic_checks(copie, extra_answers, package_name="example assert all_found ## Initialize local git repository and add ALL new files to it. - git_results = subprocess.run( - ["git", "config", "--global", "init.defaultBranch", "main"], cwd=result.project_dir, check=False - ) - assert git_results.returncode == 0 git_results = subprocess.run(["git", "init", "."], cwd=result.project_dir, check=False) assert git_results.returncode == 0 git_results = subprocess.run(["git", "add", "."], cwd=result.project_dir, check=False)