diff --git a/pyproject.toml b/pyproject.toml index e5472f3..b7c4dc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,10 +92,21 @@ exclude_lines = [ ] [tool.pyright] +# `requires-python` is the floor. Without this, pyright checks against whatever +# interpreter the job installed (3.12 in CI), so a 3.11-only annotation passes. +pythonVersion = "3.10" #typeCheckingMode = "strict" reportUnnecessaryTypeIgnoreComment = true reportMissingTypeStubs = false reportUnusedCallResult = false +# Every parameter, return and variable in the package is annotated; these keep it so. +# `reportUnknownMemberType` is left out: it gates the typing of our dependencies rather +# than of this package, and pytest types `request.node` as `Any` on purpose. +# https://github.com/pytest-dev/pytest/issues/13888#issuecomment-3511168937 +reportMissingParameterType = "error" +reportUnknownParameterType = "error" +reportUnknownVariableType = "error" +reportUnknownArgumentType = "error" reportExplicitAny = false reportAny = false include = ["pytest_examples"] diff --git a/pytest_examples/__init__.py b/pytest_examples/__init__.py index b58a304..afc2e92 100644 --- a/pytest_examples/__init__.py +++ b/pytest_examples/__init__.py @@ -13,7 +13,7 @@ __all__ = 'find_examples', 'CodeExample', 'EvalExample' -def pytest_addoption(parser) -> None: +def pytest_addoption(parser: pytest.Parser) -> None: """Add options to the pytest command line.""" group = parser.getgroup('examples') group.addoption( @@ -50,7 +50,11 @@ def _examples_to_update(pytestconfig: pytest.Config) -> Iterator[list[CodeExampl @pytest.fixture(name='eval_example') -def eval_example(tmp_path: Path, request: pytest.FixtureRequest, _examples_to_update) -> Iterator[EvalExample]: +def eval_example( + tmp_path: Path, + request: pytest.FixtureRequest, + _examples_to_update: list[CodeExample], +) -> Iterator[EvalExample]: """Fixture to return a `EvalExample` instance for running and linting examples.""" eval_ex = EvalExample(tmp_path=tmp_path, pytest_request=request) yield eval_ex diff --git a/pytest_examples/config.py b/pytest_examples/config.py index ced2099..842d7a4 100644 --- a/pytest_examples/config.py +++ b/pytest_examples/config.py @@ -30,7 +30,7 @@ class ExamplesConfig: white_space_dot: bool = False """If True, replace spaces with `ยท` in example diffs.""" - def black_mode(self): + def black_mode(self) -> BlackMode: return BlackMode( line_length=self.line_length, target_versions={BlackTargetVersion[self.target_version.upper()]} if self.target_version else set(), diff --git a/pytest_examples/eval_example.py b/pytest_examples/eval_example.py index 5b1b9c5..0b7b9cf 100644 --- a/pytest_examples/eval_example.py +++ b/pytest_examples/eval_example.py @@ -23,10 +23,10 @@ class EvalExample: """Class to run and lint examples.""" - def __init__(self, *, tmp_path: Path, pytest_request: pytest.FixtureRequest): + def __init__(self, *, tmp_path: Path, pytest_request: pytest.FixtureRequest) -> None: self.tmp_path = tmp_path self._pytest_config = pytest_request.config - self._test_id = pytest_request.node.nodeid + self._test_id: str = pytest_request.node.nodeid self.to_update: list[CodeExample] = [] self.config: ExamplesConfig = ExamplesConfig() self.print_callback: Callable[[str], str] | None = None @@ -44,7 +44,7 @@ def set_config( ruff_line_length: int | None = None, ruff_select: list[str] | None = None, ruff_ignore: list[str] | None = None, - ): + ) -> None: """Set the config for lints. Args: diff --git a/pytest_examples/find_examples.py b/pytest_examples/find_examples.py index 123bb8d..527e5d5 100644 --- a/pytest_examples/find_examples.py +++ b/pytest_examples/find_examples.py @@ -48,7 +48,7 @@ def create( end_index: int | None = None, prefix: str = '', indent: int = 0, - ): + ) -> CodeExample: """Create a `CodeExample`, mostly for testing.""" if end_line is None: end_line = start_line + source.count('\n') @@ -75,7 +75,7 @@ def prefix_settings(self) -> dict[str, str]: This works on the format `py foo="bar" spam="with space"`. """ - settings = {} + settings: dict[str, str] = {} for m in re.finditer(r'([^{\s]+?)=([\'"])(.+?)\2', self.prefix): settings[m.group(1)] = m.group(3) return settings @@ -98,7 +98,7 @@ def in_py_file(self) -> bool: """Whether the example is in a Python file.""" return self.path.suffix == '.py' - def __str__(self): + def __str__(self) -> str: try: path = self.path.relative_to(Path.cwd()) except ValueError: diff --git a/pytest_examples/lint.py b/pytest_examples/lint.py index 07d22b0..e05a3e2 100644 --- a/pytest_examples/lint.py +++ b/pytest_examples/lint.py @@ -55,7 +55,7 @@ def ruff_check( stdout, stderr = p.communicate(example.source, timeout=10) if p.returncode == 1 and stdout: - def replace_offset(m: re.Match[str]): + def replace_offset(m: re.Match[str]) -> str: line_number = int(m.group(1)) return f'{example.path}:{line_number + example.start_line}' diff --git a/pytest_examples/run_code.py b/pytest_examples/run_code.py index 4b2d446..3cbabe5 100644 --- a/pytest_examples/run_code.py +++ b/pytest_examples/run_code.py @@ -12,7 +12,7 @@ from importlib.abc import Loader from pathlib import Path from textwrap import indent -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeAlias from unittest.mock import patch import pytest @@ -28,7 +28,7 @@ __all__ = 'run_code', 'InsertPrintStatements', 'IncludePrint' parent_frame_id = 4 -IncludePrint = Callable[[Path, inspect.FrameInfo, Sequence[Any]], bool] +IncludePrint: TypeAlias = Callable[[Path, inspect.FrameInfo, Sequence[Any]], bool] def run_code( @@ -102,13 +102,14 @@ class Arg: data: str is_str: bool = False - def __init__(self, v: Any): + def __init__(self, v: Any) -> None: if isinstance(v, str): self.data = v self.is_str = True elif isinstance(v, set): # NOTE! this is not recursive - ordered = ', '.join(repr(x) for x in sorted(v)) + items: set[Any] = v + ordered = ', '.join(repr(x) for x in sorted(items)) self.data = f'{{{ordered}}}' else: self.data = re.sub('0x[a-f0-9]{8,12}>', '0x0123456789ab>', str(v)) @@ -134,16 +135,10 @@ class PrintStatement: sep: str args: list[Arg] - def __str__(self): + def __str__(self) -> str: return self.sep.join(map(str, self.args)) -def not_print(*args): - import sys - - sys.stdout.write(' '.join(map(str, args)) + '\n') - - class MockPrintFunction: __slots__ = 'file', 'statements', 'include_print' @@ -186,7 +181,7 @@ def __init__( enable: bool, print_callback: Callable[[str], str] | None, include_print: IncludePrint | None, - ): + ) -> None: self.file = python_path self.config = config self.print_func = MockPrintFunction(python_path, include_print) if enable else None @@ -198,7 +193,7 @@ def __enter__(self) -> None: self.patch = patch('builtins.print', side_effect=self.print_func) self.patch.start() - def __exit__(self, *args) -> None: + def __exit__(self, *args: Any) -> None: if self.patch is not None: self.patch.stop() diff --git a/pytest_examples/traceback.py b/pytest_examples/traceback.py index 203db49..5751df3 100644 --- a/pytest_examples/traceback.py +++ b/pytest_examples/traceback.py @@ -15,7 +15,7 @@ def create_example_traceback(exc: Exception, module_path: str, example: CodeExam Frames outside the example are not included in the new traceback. """ - frames = [] + frames: list[tuple[FrameType, int, int]] = [] tb = exc.__traceback__ while tb is not None: frame = tb.tb_frame