From 5aaa7640ccde39a44a60a3e95a47861af02df78c Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 14 Mar 2025 12:23:41 +0000 Subject: [PATCH 1/3] Support pyright strict mode in template - Add an option to the template to enable strict mode if pyright is selected as the type checker. - Default to standard type checking, the user must actively select it - Set config to strict if the user selects both pyright and strict mode (we do not support mypy strict mode) - Modify tests, remove "manual" enabling of strict mode now the template can do it for us --- copier.yml | 12 ++++++++++++ example-answers.yml | 1 + template/pyproject.toml.jinja | 6 ++++-- tests/test_example.py | 21 ++++++++++++++------- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/copier.yml b/copier.yml index 046b88b2..38af1c73 100644 --- a/copier.yml +++ b/copier.yml @@ -122,6 +122,18 @@ type_checker: - pyright - mypy +strict_typing: + type: bool + when: >- + {{ type_checker == 'pyright' }} + default: false + help: | + Would you like to run pyright in strict mode? + It is excellent for building consistent, maintainable projects but difficult to adopt on top + of legacy code and projects with many external dependencies. + The recommended approach is to start with strict mode and disable it if it + becomes too costly to maintain. + pypi: type: bool help: Would you like the wheel and source distribution to be automatically uploaded to PyPI when a release is made? diff --git a/example-answers.yml b/example-answers.yml index 0ff28f7f..1b1c689b 100644 --- a/example-answers.yml +++ b/example-answers.yml @@ -12,4 +12,5 @@ github_org: DiamondLightSource package_name: python_copier_template_example repo_name: python-copier-template-example type_checker: pyright +strict_typing: false pypi: true diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 9fe0ac81..93302aae 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -51,9 +51,11 @@ name = "{{ author_name }}" [tool.setuptools_scm] version_file = "src/{{ package_name }}/_version.py" {% if type_checker=="pyright" %} -[tool.pyright] +[tool.pyright]{% if strict_typing %} +typeCheckingMode = "strict" +{% else %} typeCheckingMode = "standard" -reportMissingImports = false # Ignore missing stubs in imported modules +{% endif %}reportMissingImports = false # Ignore missing stubs in imported modules {% endif %}{% if type_checker=="mypy" %} [tool.mypy] ignore_missing_imports = true # Ignore missing stubs in imported modules diff --git a/tests/test_example.py b/tests/test_example.py index 015bc04f..ffe15b47 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -48,9 +48,11 @@ def test_template_defaults(tmp_path: Path): copy_project(tmp_path) run = make_venv(tmp_path) container_doc = tmp_path / "docs" / "how-to" / "run-container.md" + pyproject_toml = tmp_path / "pyproject.toml" assert container_doc.exists() catalog_info = tmp_path / "catalog-info.yaml" assert catalog_info.exists() + assert 'typeCheckingMode = "standard"' in pyproject_toml.read_text() run("./venv/bin/tox -p") if not run_pipe("git tag --points-at HEAD"): # Only run linkcheck if not on a tag, as the CI might not have pushed @@ -213,21 +215,26 @@ def __init__(self): run("ruff check") -def test_works_in_pyright_strict_mode(tmp_path: Path): - copy_project(tmp_path) +def test_pyright_works_in_strict_typing_mode(tmp_path: Path): + copy_project(tmp_path, type_checker="pyright", strict_typing=True) pyproject_toml = tmp_path / "pyproject.toml" - # Enable strict mode - run_pipe( - 'sed -i \'s|typeCheckingMode = "standard"|typeCheckingMode = "strict"|\'' - f" {pyproject_toml}" - ) + # Check strict mode is configured + assert 'typeCheckingMode = "strict"' in pyproject_toml.read_text() # Ensure pyright is still happy run = make_venv(tmp_path) run(f"./venv/bin/pyright {tmp_path}") +def test_ignores_mypy_strict_mode(tmp_path: Path): + copy_project(tmp_path, type_checker="mypy", strict_typing=True) + pyproject_toml = tmp_path / "pyproject.toml" + + # Check strict mode is not configured + assert "typeCheckingMode =" not in pyproject_toml.read_text() + + def test_works_with_pydocstyle(tmp_path: Path): copy_project(tmp_path) pyproject_toml = tmp_path / "pyproject.toml" From 972c1ce0e448713997f0f1d4e089c760cbe60471 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 14 Mar 2025 12:38:31 +0000 Subject: [PATCH 2/3] Update docs on strict mode Update docs on strict mode with more explanation of when to/not to use it. Include link in template prompt. --- copier.yml | 6 +++--- docs/how-to/strict-mode.md | 18 +++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/copier.yml b/copier.yml index 38af1c73..d8f20b3e 100644 --- a/copier.yml +++ b/copier.yml @@ -129,10 +129,10 @@ strict_typing: default: false help: | Would you like to run pyright in strict mode? - It is excellent for building consistent, maintainable projects but difficult to adopt on top - of legacy code and projects with many external dependencies. The recommended approach is to start with strict mode and disable it if it - becomes too costly to maintain. + becomes too costly to maintain. + See https://diamondlightsource.github.io/python-copier-template/main/how-to/strict-mode.html + for more information. pypi: type: bool diff --git a/docs/how-to/strict-mode.md b/docs/how-to/strict-mode.md index 8c9dac53..d3842e19 100644 --- a/docs/how-to/strict-mode.md +++ b/docs/how-to/strict-mode.md @@ -1,17 +1,13 @@ -# Enable Pyright's Strict Mode +# Use Pyright's Strict Mode -For projects using pyright you can enable strict mode for stricter than normal type checking. See [the docs](https://github.com/microsoft/pyright/blob/main/docs/configuration.md) for a full breakdown. The primary benefits are increased confidence in code that has been more thoroughly analyzed and a shorter development time thanks to fast feedback from the type checker. +For projects using pyright you can enable strict mode for stricter than normal type checking. See [the docs](https://github.com/microsoft/pyright/blob/main/docs/configuration.md) for a full breakdown. -## Configuration +## How to Enable -Change the `typeCheckingMode` line to `"strict"` in `pyproject.toml` as follows: +When creating a template, select `pyright` as the type checker and type `y` when prompted to enable strict mode. -```toml -[tool.pyright] -typeCheckingMode = "strict" -reportMissingImports = false # Ignore missing stubs in imported modules -``` +## Who Should Use Strict Mode? -## Third Party Libraries +Strict mode enforces good practices such as type hints on function signatures, providing increased confidence in code that has been more thoroughly analyzed and a shorter development time thanks to fast feedback from the type checker. Starting a new project and continually keeping it passing provides a long-term benefit when it comes to maintanability and robustness. However, adopting strict mode on top of legacy projects is likely to lead to lots of errors to work through - probably thousands. Additionally it does not usually work well with libraries that do not have [type stubs](https://github.com/microsoft/pyright/blob/main/docs/type-stubs.md), you will likely need a `# type: ignore` on any line that directly uses the library code. This may limit the usefulness of pyright but it can still be worth doing to ensure your own code is internally consistent. -Strict mode does not usually work well with libraries that do not have [type stubs](https://github.com/microsoft/pyright/blob/main/docs/type-stubs.md), you will likely need a `# type: ignore` on any line that directly uses the library code. This may limit the usefulness of pyright but it can still be worth doing to ensure your own code is internally consistent. +The recommended approach for brand new projects is to enable strict mode and stick with it for as long as is practical, moving away if it starts to cause more hindrance than help (e.g. because too many major dependencies do not support it). From 6372ae3ada602d8845c0bde1d1ff4a0aca8fd07e Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 14 Mar 2025 13:11:33 +0000 Subject: [PATCH 3/3] Make strict mode the default Set the default state of the template to pyright with strict mode, the user must conciously disable it. This is in line with the recommended approach in the docs of using strict mode fro new projects if practical. --- copier.yml | 2 +- example-answers.yml | 2 +- tests/test_example.py | 20 ++++++++++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/copier.yml b/copier.yml index d8f20b3e..ea1ecc1e 100644 --- a/copier.yml +++ b/copier.yml @@ -126,7 +126,7 @@ strict_typing: type: bool when: >- {{ type_checker == 'pyright' }} - default: false + default: true help: | Would you like to run pyright in strict mode? The recommended approach is to start with strict mode and disable it if it diff --git a/example-answers.yml b/example-answers.yml index 1b1c689b..291daedf 100644 --- a/example-answers.yml +++ b/example-answers.yml @@ -12,5 +12,5 @@ github_org: DiamondLightSource package_name: python_copier_template_example repo_name: python-copier-template-example type_checker: pyright -strict_typing: false +strict_typing: true pypi: true diff --git a/tests/test_example.py b/tests/test_example.py index ffe15b47..a8c0f9ac 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -52,7 +52,7 @@ def test_template_defaults(tmp_path: Path): assert container_doc.exists() catalog_info = tmp_path / "catalog-info.yaml" assert catalog_info.exists() - assert 'typeCheckingMode = "standard"' in pyproject_toml.read_text() + assert 'typeCheckingMode = "strict"' in pyproject_toml.read_text() run("./venv/bin/tox -p") if not run_pipe("git tag --points-at HEAD"): # Only run linkcheck if not on a tag, as the CI might not have pushed @@ -70,8 +70,16 @@ def test_template_with_extra_code_and_api_docs(tmp_path: Path): init = tmp_path / "src" / "python_copier_template_example" / "__init__.py" init.write_text( init.read_text().replace( - "__all__ = [", + """ +from ._version import __version__ + +__all__ = [""", ''' +from python_copier_template_example import extra_pkg + +from ._version import __version__ + + class TopCls: """A top level class.""" @@ -215,12 +223,12 @@ def __init__(self): run("ruff check") -def test_pyright_works_in_strict_typing_mode(tmp_path: Path): - copy_project(tmp_path, type_checker="pyright", strict_typing=True) +def test_pyright_works_in_standard_typing_mode(tmp_path: Path): + copy_project(tmp_path, type_checker="pyright", strict_typing=False) pyproject_toml = tmp_path / "pyproject.toml" - # Check strict mode is configured - assert 'typeCheckingMode = "strict"' in pyproject_toml.read_text() + # Check standard mode is configured + assert 'typeCheckingMode = "standard"' in pyproject_toml.read_text() # Ensure pyright is still happy run = make_venv(tmp_path)