From 006659da75a9a67ea003ce63618f707900c389ab Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Wed, 2 Jul 2025 15:22:16 -0700 Subject: [PATCH 1/4] add mypy support --- copier.yml | 4 +- includes/pyproject/deps-pep-621.toml.jinja | 2 +- pyproject.toml | 1 + template/pyproject.toml.jinja | 36 ++++++-------- .../workflows/test.yml.jinja | 25 +++++++++- tests/conftest.py | 3 +- tests/test_template_init.py | 49 ++++++++++++++++++- 7 files changed, 91 insertions(+), 29 deletions(-) diff --git a/copier.yml b/copier.yml index e812aad..da84d16 100644 --- a/copier.yml +++ b/copier.yml @@ -151,11 +151,11 @@ use_precommit: default: "{% if template_mode != 'minimal' %}yes{% else %}no{% endif %}" help: "Do you want to pre-commit hooks to format your code on save?" -use_types: +use_mypy: when: "{{ template_mode == 'custom' }}" type: bool default: "{% if template_mode != 'minimal' %}yes{% else %}no{% endif %}" - help: "Do you want to use typing annotations and type check your code?" + help: "Do you want to use mypy to check type annotations?" ############### All things related to testing ################### diff --git a/includes/pyproject/deps-pep-621.toml.jinja b/includes/pyproject/deps-pep-621.toml.jinja index b7dc4b1..83c86ac 100644 --- a/includes/pyproject/deps-pep-621.toml.jinja +++ b/includes/pyproject/deps-pep-621.toml.jinja @@ -27,7 +27,7 @@ style = [ "ruff", ] {% endif %} -{%- if use_types -%} +{%- if use_mypy -%} types = [ "mypy", ] diff --git a/pyproject.toml b/pyproject.toml index 86a23f9..36635cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ ignore = [ "S602", "S603", "S607", + "FBT001", # boolean positional arguments are fine always but especially in tests ] [tool.ruff.lint.isort] diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 2f863fa..c45c0e9 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -45,7 +45,7 @@ classifiers = [ {% endif -%} "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", - {%- if use_types %} + {%- if use_mypy %} "Typing :: Typed", {%- endif %} ] @@ -72,7 +72,7 @@ dev = [ {%- if documentation!="" %}docs,{% endif -%} {%- if use_test %}tests,{% endif -%} {%- if use_lint %}style,{% endif -%} - {%- if use_types %}types,{% endif -%} + {%- if use_mypy %}types,{% endif -%} audit]", {%- endif %} ] @@ -179,7 +179,7 @@ lines-after-imports = 1 [tool.pydoclint] style = "numpy" -{%- if use_types %} +{%- if use_mypy %} arg-type-hints-in-signature = true {%- else %} arg-type-hints-in-signature = false @@ -192,24 +192,16 @@ exclude = "_version.py" {%- endif %} {%- endif %} -{%- if use_types %} -# TODO: Adjust mypy configuration. -#[tool.mypy] -#plugins = [ -# "pydantic.mypy", -#] - -# Stop mypy from complaining about missing types from imports. -#[[tool.mypy.overrides]] -#module = [ -# "pandas", -#] -#ignore_missing_imports = true - -#[tool.pydantic-mypy] -#init_forbid_extra = true -#init_typed = true -#warn_required_dynamic_aliases = true +{%- if use_mypy %} +[tool.mypy] +packages = ["{{ package_name }}"] +warn_redundant_casts = true +warn_unused_ignores = true +show_error_context = true +show_column_numbers = true +show_error_code_links = true +pretty = true +color_output = true {%- endif %} {%- if use_hatch_envs %} @@ -242,7 +234,7 @@ extra-dependencies = [ [tool.hatch.envs.audit.scripts] check = ["pip-audit"] -{%- if use_types %} +{%- if use_mypy %} [tool.hatch.envs.types] description = """Check the static types of the codebase.""" dependencies = [ diff --git a/template/{% if use_git and dev_platform == 'GitHub' %}.github{% endif %}/workflows/test.yml.jinja b/template/{% if use_git and dev_platform == 'GitHub' %}.github{% endif %}/workflows/test.yml.jinja index 7542bb3..a45fd50 100644 --- a/template/{% if use_git and dev_platform == 'GitHub' %}.github{% endif %}/workflows/test.yml.jinja +++ b/template/{% if use_git and dev_platform == 'GitHub' %}.github{% endif %}/workflows/test.yml.jinja @@ -67,7 +67,7 @@ jobs: - name: Check dependencies run: hatch run audit:check -{%- if use_types %} +{%- if use_mypy %} - name: Check types run: hatch run types:check {%- endif %} @@ -82,4 +82,27 @@ jobs: run: bash <(curl -s https://codecov.io/bash) {%- endraw %} {%- endif %} +{%- else %} + + - name: Install package + run: python -m pip install '.[dev]' + + - name: Check dependencies + run: python -m pip-audit + +{%- if use_mypy %} + + - name: Check types + run: python -m mypy +{%- endif %} + +{%- if use_test %} + + - name: Run tests + run: python -m pytest --cov={{ package_name }} --cov-report=term-missing + + - name: Report coverage + shell: bash + run: bash <(curl -s https://codecov.io/bash) +{%- endif %} {%- endif %} diff --git a/tests/conftest.py b/tests/conftest.py index 659a3e8..36e7b5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,9 @@ """Provide fixtures to the entire test suite.""" import shutil +from collections.abc import Generator from pathlib import Path -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING import pytest from _pytest.monkeypatch import MonkeyPatch diff --git a/tests/test_template_init.py b/tests/test_template_init.py index 4e1e2f5..0eb35c8 100644 --- a/tests/test_template_init.py +++ b/tests/test_template_init.py @@ -22,8 +22,10 @@ import os import subprocess import sys +import venv from datetime import datetime, timezone from pathlib import Path +from textwrap import dedent from typing import Callable import pytest @@ -196,7 +198,7 @@ def test_docs_build(documentation: str, generated: Callable[..., Path], use_hatc ) run_command("sphinx-apidoc -o docs/api src/alien_clones", project) # prepend pythonpath so we don't have to actually install here... - run_command(f"PYTHONPATH={str(project/'src')} sphinx-build -W -b html docs docs/_build", project) + run_command(f"PYTHONPATH={project/'src'!s} sphinx-build -W -b html docs docs/_build", project) run_command("pre-commit run --all-files -v check-readthedocs", project) @@ -229,7 +231,7 @@ def test_non_hatch_deps( project = generated( use_hatch_envs=False, use_lint=True, - use_types=True, + use_mypy=True, use_test=True, use_git=False, documentation=documentation, @@ -254,3 +256,46 @@ def test_non_hatch_deps( if documentation != "no": assert "docs" in optional_deps assert any(dep.startswith(documentation) for dep in optional_deps["docs"]) + +@pytest.mark.parametrize("use_hatch_envs", [True, False]) +@pytest.mark.parametrize("valid", [True, False]) +def test_mypy(generated: Callable[..., Path], use_hatch_envs: bool, valid: bool, tmp_path: Path): + """Mypy type checking works out of the box.""" + if valid: + module = dedent(""" + def f1(value: int, other: int = 2) -> int: + return value * other + + def f2(value: int) -> float: + return f1(value, 5) / 2 + """) + else: + module = dedent(""" + def f1(value: int, other: int = 2) -> int: + return value * other + + def f2(value: str) -> str: + return f1(value, 5) / 2 + """) + + root = generated(use_hatch_envs=use_hatch_envs, use_mypy=True) + pkg_path = root / "src" / "alien_clones" + module_path = pkg_path / "typechecking.py" + with module_path.open("w") as f: + f.write(module) + + if use_hatch_envs: + command = "hatch run types:check" + else: + venv_path = tmp_path / ".venv" + mypy_path = venv_path / "bin" / "mypy" + python_path = venv_path / "bin" / "python" + venv.EnvBuilder(with_pip=True).create(venv_path) + run_command(f"{python_path!s} -m pip install -e '.[types]'", root) + command = f"{mypy_path!s} {pkg_path!s}" + + if valid: + run_command(command, root) + else: + with pytest.raises(subprocess.CalledProcessError): + run_command(command, root) From 932f77ceb8122379037a681c4bd9943a27cd2453 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Wed, 2 Jul 2025 15:22:16 -0700 Subject: [PATCH 2/4] add mypy support --- copier.yml | 4 +- includes/pyproject/deps-pep-621.toml.jinja | 2 +- pyproject.toml | 1 + template/pyproject.toml.jinja | 36 ++++++-------- .../workflows/test.yml.jinja | 25 +++++++++- tests/conftest.py | 3 +- tests/test_template_init.py | 49 ++++++++++++++++++- 7 files changed, 91 insertions(+), 29 deletions(-) diff --git a/copier.yml b/copier.yml index e812aad..da84d16 100644 --- a/copier.yml +++ b/copier.yml @@ -151,11 +151,11 @@ use_precommit: default: "{% if template_mode != 'minimal' %}yes{% else %}no{% endif %}" help: "Do you want to pre-commit hooks to format your code on save?" -use_types: +use_mypy: when: "{{ template_mode == 'custom' }}" type: bool default: "{% if template_mode != 'minimal' %}yes{% else %}no{% endif %}" - help: "Do you want to use typing annotations and type check your code?" + help: "Do you want to use mypy to check type annotations?" ############### All things related to testing ################### diff --git a/includes/pyproject/deps-pep-621.toml.jinja b/includes/pyproject/deps-pep-621.toml.jinja index b7dc4b1..83c86ac 100644 --- a/includes/pyproject/deps-pep-621.toml.jinja +++ b/includes/pyproject/deps-pep-621.toml.jinja @@ -27,7 +27,7 @@ style = [ "ruff", ] {% endif %} -{%- if use_types -%} +{%- if use_mypy -%} types = [ "mypy", ] diff --git a/pyproject.toml b/pyproject.toml index 86a23f9..36635cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ ignore = [ "S602", "S603", "S607", + "FBT001", # boolean positional arguments are fine always but especially in tests ] [tool.ruff.lint.isort] diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 2f863fa..c45c0e9 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -45,7 +45,7 @@ classifiers = [ {% endif -%} "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", - {%- if use_types %} + {%- if use_mypy %} "Typing :: Typed", {%- endif %} ] @@ -72,7 +72,7 @@ dev = [ {%- if documentation!="" %}docs,{% endif -%} {%- if use_test %}tests,{% endif -%} {%- if use_lint %}style,{% endif -%} - {%- if use_types %}types,{% endif -%} + {%- if use_mypy %}types,{% endif -%} audit]", {%- endif %} ] @@ -179,7 +179,7 @@ lines-after-imports = 1 [tool.pydoclint] style = "numpy" -{%- if use_types %} +{%- if use_mypy %} arg-type-hints-in-signature = true {%- else %} arg-type-hints-in-signature = false @@ -192,24 +192,16 @@ exclude = "_version.py" {%- endif %} {%- endif %} -{%- if use_types %} -# TODO: Adjust mypy configuration. -#[tool.mypy] -#plugins = [ -# "pydantic.mypy", -#] - -# Stop mypy from complaining about missing types from imports. -#[[tool.mypy.overrides]] -#module = [ -# "pandas", -#] -#ignore_missing_imports = true - -#[tool.pydantic-mypy] -#init_forbid_extra = true -#init_typed = true -#warn_required_dynamic_aliases = true +{%- if use_mypy %} +[tool.mypy] +packages = ["{{ package_name }}"] +warn_redundant_casts = true +warn_unused_ignores = true +show_error_context = true +show_column_numbers = true +show_error_code_links = true +pretty = true +color_output = true {%- endif %} {%- if use_hatch_envs %} @@ -242,7 +234,7 @@ extra-dependencies = [ [tool.hatch.envs.audit.scripts] check = ["pip-audit"] -{%- if use_types %} +{%- if use_mypy %} [tool.hatch.envs.types] description = """Check the static types of the codebase.""" dependencies = [ diff --git a/template/{% if use_git and dev_platform == 'GitHub' %}.github{% endif %}/workflows/test.yml.jinja b/template/{% if use_git and dev_platform == 'GitHub' %}.github{% endif %}/workflows/test.yml.jinja index 7542bb3..a45fd50 100644 --- a/template/{% if use_git and dev_platform == 'GitHub' %}.github{% endif %}/workflows/test.yml.jinja +++ b/template/{% if use_git and dev_platform == 'GitHub' %}.github{% endif %}/workflows/test.yml.jinja @@ -67,7 +67,7 @@ jobs: - name: Check dependencies run: hatch run audit:check -{%- if use_types %} +{%- if use_mypy %} - name: Check types run: hatch run types:check {%- endif %} @@ -82,4 +82,27 @@ jobs: run: bash <(curl -s https://codecov.io/bash) {%- endraw %} {%- endif %} +{%- else %} + + - name: Install package + run: python -m pip install '.[dev]' + + - name: Check dependencies + run: python -m pip-audit + +{%- if use_mypy %} + + - name: Check types + run: python -m mypy +{%- endif %} + +{%- if use_test %} + + - name: Run tests + run: python -m pytest --cov={{ package_name }} --cov-report=term-missing + + - name: Report coverage + shell: bash + run: bash <(curl -s https://codecov.io/bash) +{%- endif %} {%- endif %} diff --git a/tests/conftest.py b/tests/conftest.py index 659a3e8..36e7b5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,9 @@ """Provide fixtures to the entire test suite.""" import shutil +from collections.abc import Generator from pathlib import Path -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING import pytest from _pytest.monkeypatch import MonkeyPatch diff --git a/tests/test_template_init.py b/tests/test_template_init.py index 4e1e2f5..0eb35c8 100644 --- a/tests/test_template_init.py +++ b/tests/test_template_init.py @@ -22,8 +22,10 @@ import os import subprocess import sys +import venv from datetime import datetime, timezone from pathlib import Path +from textwrap import dedent from typing import Callable import pytest @@ -196,7 +198,7 @@ def test_docs_build(documentation: str, generated: Callable[..., Path], use_hatc ) run_command("sphinx-apidoc -o docs/api src/alien_clones", project) # prepend pythonpath so we don't have to actually install here... - run_command(f"PYTHONPATH={str(project/'src')} sphinx-build -W -b html docs docs/_build", project) + run_command(f"PYTHONPATH={project/'src'!s} sphinx-build -W -b html docs docs/_build", project) run_command("pre-commit run --all-files -v check-readthedocs", project) @@ -229,7 +231,7 @@ def test_non_hatch_deps( project = generated( use_hatch_envs=False, use_lint=True, - use_types=True, + use_mypy=True, use_test=True, use_git=False, documentation=documentation, @@ -254,3 +256,46 @@ def test_non_hatch_deps( if documentation != "no": assert "docs" in optional_deps assert any(dep.startswith(documentation) for dep in optional_deps["docs"]) + +@pytest.mark.parametrize("use_hatch_envs", [True, False]) +@pytest.mark.parametrize("valid", [True, False]) +def test_mypy(generated: Callable[..., Path], use_hatch_envs: bool, valid: bool, tmp_path: Path): + """Mypy type checking works out of the box.""" + if valid: + module = dedent(""" + def f1(value: int, other: int = 2) -> int: + return value * other + + def f2(value: int) -> float: + return f1(value, 5) / 2 + """) + else: + module = dedent(""" + def f1(value: int, other: int = 2) -> int: + return value * other + + def f2(value: str) -> str: + return f1(value, 5) / 2 + """) + + root = generated(use_hatch_envs=use_hatch_envs, use_mypy=True) + pkg_path = root / "src" / "alien_clones" + module_path = pkg_path / "typechecking.py" + with module_path.open("w") as f: + f.write(module) + + if use_hatch_envs: + command = "hatch run types:check" + else: + venv_path = tmp_path / ".venv" + mypy_path = venv_path / "bin" / "mypy" + python_path = venv_path / "bin" / "python" + venv.EnvBuilder(with_pip=True).create(venv_path) + run_command(f"{python_path!s} -m pip install -e '.[types]'", root) + command = f"{mypy_path!s} {pkg_path!s}" + + if valid: + run_command(command, root) + else: + with pytest.raises(subprocess.CalledProcessError): + run_command(command, root) From 976f8ba4dd1d50470322a2574f8e33d616ca8f08 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Wed, 2 Jul 2025 15:45:01 -0700 Subject: [PATCH 3/4] does windows shell care about double quotes lol --- tests/test_template_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_template_init.py b/tests/test_template_init.py index 0eb35c8..e7259ee 100644 --- a/tests/test_template_init.py +++ b/tests/test_template_init.py @@ -291,7 +291,7 @@ def f2(value: str) -> str: mypy_path = venv_path / "bin" / "mypy" python_path = venv_path / "bin" / "python" venv.EnvBuilder(with_pip=True).create(venv_path) - run_command(f"{python_path!s} -m pip install -e '.[types]'", root) + run_command(f'{python_path!s} -m pip install -e ".[types]"', root) command = f"{mypy_path!s} {pkg_path!s}" if valid: From d5c83e02c70ba7a745a2d2e2b3858250c2215e8e Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Wed, 2 Jul 2025 16:08:21 -0700 Subject: [PATCH 4/4] windows istg --- tests/test_template_init.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_template_init.py b/tests/test_template_init.py index e7259ee..5096d90 100644 --- a/tests/test_template_init.py +++ b/tests/test_template_init.py @@ -288,8 +288,12 @@ def f2(value: str) -> str: command = "hatch run types:check" else: venv_path = tmp_path / ".venv" - mypy_path = venv_path / "bin" / "mypy" - python_path = venv_path / "bin" / "python" + if sys.platform == "win32": + mypy_path = venv_path / "Scripts" / "mypy.exe" + python_path = venv_path / "Scripts" / "python.exe" + else: + mypy_path = venv_path / "bin" / "mypy" + python_path = venv_path / "bin" / "python" venv.EnvBuilder(with_pip=True).create(venv_path) run_command(f'{python_path!s} -m pip install -e ".[types]"', root) command = f"{mypy_path!s} {pkg_path!s}"