diff --git a/README.md b/README.md index 18c2bc25..0317eccd 100644 --- a/README.md +++ b/README.md @@ -361,9 +361,9 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1 - [`GH211`](https://learn.scientific-python.org/development/guides/gha-basic#GH211): Do not pin core actions as major versions - [`GH212`](https://learn.scientific-python.org/development/guides/gha-basic#GH212): Require GHA update grouping -### MyPy +### Type checking -- [`MY100`](https://learn.scientific-python.org/development/guides/style#MY100): Uses a type checker (pyproject config) +- [`MY100`](https://learn.scientific-python.org/development/guides/style#MY100): Uses a type checker - [`MY101`](https://learn.scientific-python.org/development/guides/style#MY101): Type checker in strict mode - `MY102`: MyPy show_error_codes deprecated - [`MY103`](https://learn.scientific-python.org/development/guides/style#MY103): MyPy warn unreachable diff --git a/docs/guides/index.md b/docs/guides/index.md index 1a2727b9..89a5e420 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -9,7 +9,7 @@ for your `CONTRIBUTING.md`, and at least glance through other sections. Following that, there are recommendations for [style][], intended to promote good practices and to ensure continuity across the packages. There is a -[dedicated page for static type checking with MyPy][mypy]. There are then +[dedicated page for static type checking][typing]. There are then guides on [simple packaging][] and [compiled packaging][], which should help in ensuring a consistent developer and user experience when working with distribution. @@ -38,7 +38,7 @@ WebAssembly! All checks point to a linked badge in the guide. [tutorials]: /tutorials/index.md [style]: /guides/style.md -[mypy]: /guides/typing.md +[typing]: /guides/typing.md [docs]: /guides/docs.md [simple packaging]: /guides/packaging_simple.md [compiled packaging]: /guides/packaging_compiled.md diff --git a/docs/guides/style.md b/docs/guides/style.md index 9ff4d0f1..f1b452a0 100644 --- a/docs/guides/style.md +++ b/docs/guides/style.md @@ -690,7 +690,9 @@ another. ::: :::: -These checkers read their configuration from `pyproject.toml`: +These checkers read their configuration from `pyproject.toml`, shown below. +Pyrefly and ty can also use a standalone `pyrefly.toml` or `ty.toml` file. In +these files, drop the `tool.` prefix from the table names. ::::{tab-set} :::{tab-item} MyPy diff --git a/docs/guides/typing.md b/docs/guides/typing.md index ead3d94a..b9a40d42 100644 --- a/docs/guides/typing.md +++ b/docs/guides/typing.md @@ -53,17 +53,53 @@ code also runs. ### Configuration -By default, the type checker does as little as possible, so that you can add it -iteratively to a code base. By default: +The three main type checkers are [MyPy](https://mypy.readthedocs.io), +[Pyrefly](https://pyrefly.org), and [ty](https://docs.astral.sh/ty/). They +follow the same typing specification, but their defaults are different. + +Each checker reads configuration from `pyproject.toml`. Turn on as much +checking as you can, and increase it over time. See the [style page][] for +configuration suggestions for each checker. + +::::{tab-set} +:::{tab-item} MyPy +:sync: mypy +MyPy does as little as possible by default, so that you can add it gradually +to a code base: - All untyped variables and return values will be `Any`. - Code inside untyped functions is not checked _at all_. -You can add configuration to `pyproject.toml` (and a little bit to the files -themselves), or you can go all the way and pass `--strict`, which will turn on -everything. Try to turn on as much as possible, and increase it until you can -run with full `strict` checking. See the [style page][] for configuration -suggestions. +Set `strict = true` (or pass `--strict`) to turn on all checks. Disable MyPy on +a line with `# type: ignore[error-code]`. +::: +:::{tab-item} Pyrefly +:sync: pyrefly +Pyrefly checks the bodies of all functions by default, including untyped ones. +It also infers return types of unannotated functions, so it can find errors in +code that has no annotations. + +Set `preset = "strict"` to turn on stricter checks. Configuration can also go +in a `pyrefly.toml` file. Disable Pyrefly only on a line with `# pyrefly: +ignore`. Note that pyrefly will try to read mypy's config if no pyrefly config +is present. + +::: +:::{tab-item} ty +:sync: ty +ty checks the bodies of all functions by default, including untyped ones. It +also infers return types of unannotated functions, so it can find errors in +code that has no annotations. + +There is no single strict switch. Set the severity of individual rules in +`[tool.ty.rules]`. Configuration can also go in a `ty.toml` file. Disable ty +only on a line with `# ty: ignore`. +::: +:::: + +The checkers do not always agree, because each one implements the typing +specification differently. If your library supports more than one checker, run +each one in CI. All three support `# type: ignore`. [style page]: /guides/style.md diff --git a/src/sp_repo_review/checks/mypy.py b/src/sp_repo_review/checks/mypy.py index 4edcf315..14045515 100644 --- a/src/sp_repo_review/checks/mypy.py +++ b/src/sp_repo_review/checks/mypy.py @@ -1,30 +1,36 @@ from __future__ import annotations -__lazy_modules__ = ["typing"] - -from typing import Any +from typing import TYPE_CHECKING, Any from . import mk_url +if TYPE_CHECKING: + from importlib.resources.abc import Traversable + +MY100_CONFIG_FILES = ("ty.toml", "pyrefly.toml") + class MyPy: family = "mypy" class MY100(MyPy): - "Uses a type checker (pyproject config)" + "Uses a type checker" requires = {"PY001"} url = mk_url("style") @staticmethod - def check(pyproject: dict[str, Any]) -> bool: + def check(pyproject: dict[str, Any], package: Traversable) -> bool: """ Must have a `tool.mypy`, `tool.pyrefly`, or `tool.ty` section in - `pyproject.toml`. Other forms of configuration are not supported by - this check. + `pyproject.toml`, or a `ty.toml` or `pyrefly.toml` file. The other checks in this family only read + `pyproject.toml`. """ + if any(package.joinpath(fn).is_file() for fn in MY100_CONFIG_FILES): + return True + match pyproject: case { "tool": {"mypy": object()} | {"pyrefly": object()} | {"ty": object()} diff --git a/src/sp_repo_review/families.py b/src/sp_repo_review/families.py index 2dbbf5c1..9e8011eb 100644 --- a/src/sp_repo_review/families.py +++ b/src/sp_repo_review/families.py @@ -122,7 +122,7 @@ def get_families( readme_note="Will not show up if using lefthook instead of pre-commit/prek.", ), "mypy": Family( - name="MyPy", + name="Type checking", ), "ruff": Family( name="Ruff", diff --git a/src/sp_repo_review/files.py b/src/sp_repo_review/files.py index fee824a5..a25f085f 100644 --- a/src/sp_repo_review/files.py +++ b/src/sp_repo_review/files.py @@ -26,6 +26,8 @@ def prefetch_package() -> set[str]: ".pre-commit-config.yaml", ".readthedocs.yml", "noxfile.py", + "ty.toml", + "pyrefly.toml", "ruff.toml", ".ruff.toml", } diff --git a/tests/test_mypy.py b/tests/test_mypy.py index 19994897..386c6c50 100644 --- a/tests/test_mypy.py +++ b/tests/test_mypy.py @@ -1,32 +1,35 @@ +from pathlib import Path + +import pytest from repo_review.testing import compute_check, toml_loads -def test_my100_present(): +def test_my100_present(tmp_path: Path): toml = toml_loads(""" [tool.mypy] strict = true """) - assert compute_check("MY100", pyproject=toml).result + assert compute_check("MY100", pyproject=toml, package=tmp_path).result -def test_my100_pyrefly(): +def test_my100_pyrefly(tmp_path: Path): toml = toml_loads(""" [tool.pyrefly] project-includes = ["src"] """) - assert compute_check("MY100", pyproject=toml).result + assert compute_check("MY100", pyproject=toml, package=tmp_path).result -def test_my100_ty(): +def test_my100_ty(tmp_path: Path): toml = toml_loads(""" [tool.ty.src] include = ["src"] """) - assert compute_check("MY100", pyproject=toml).result + assert compute_check("MY100", pyproject=toml, package=tmp_path).result -def test_my100_missing(): - assert not compute_check("MY100", pyproject={}).result +def test_my100_missing(tmp_path: Path): + assert not compute_check("MY100", pyproject={}, package=tmp_path).result def test_my101_present_bool(): @@ -156,3 +159,9 @@ def test_my106_missing(): enable_error_code = ["ignore-without-code", "redundant-expr"] """) assert not compute_check("MY106", pyproject=toml).result + + +@pytest.mark.parametrize("name", ["ty.toml", "pyrefly.toml"]) +def test_my100_config_file(tmp_path: Path, name: str): + tmp_path.joinpath(name).touch() + assert compute_check("MY100", pyproject={}, package=tmp_path).result