Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/guides/style.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>` prefix from the table names.

::::{tab-set}
:::{tab-item} MyPy
Expand Down
50 changes: 43 additions & 7 deletions docs/guides/typing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 13 additions & 7 deletions src/sp_repo_review/checks/mypy.py
Original file line number Diff line number Diff line change
@@ -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()}
Expand Down
2 changes: 1 addition & 1 deletion src/sp_repo_review/families.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/sp_repo_review/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
25 changes: 17 additions & 8 deletions tests/test_mypy.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -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
Loading