diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 31d9bbb..0216782 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -95,7 +95,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install -c constraints.txt flake8 flake8-isort black mypy + python -m pip install -c constraints.txt black flake8 flake8-isort mypy pytest pip list - name: Run black run: | diff --git a/constraints.txt b/constraints.txt index f2e158e..3d61b2d 100644 --- a/constraints.txt +++ b/constraints.txt @@ -20,8 +20,12 @@ distlib==0.3.4 docutils==0.18.1 filelock==3.7.0 flake8==4.0.1 -flake8-black==0.3.3 -flake8-isort==4.1.1 +flake8-bugbear==22.7.1 +flake8-comprehensions==3.10.0 +flake8-html==0.4.2 +flake8-logging-format==0.6.0 +flake8-mutable==1.2.0 +flake8-pyi==22.7.0 fsfe-reuse==1.0.0 idna==3.3 iniconfig==1.1.1 @@ -31,7 +35,7 @@ jinja2==3.1.2 license-expression==30.0.0 markupsafe==2.1.1 mccabe==0.6.1 -mypy==0.931 +mypy==0.971 mypy-extensions==0.4.3 packaging==21.3 pathspec==0.9.0 @@ -42,8 +46,9 @@ pluggy==1.0.0 py==1.11.0 pycodestyle==2.8.0 pyflakes==2.4.0 +pygments==2.12.0 pyparsing==3.0.9 -pytest==7.0.1 +pytest==7.1.2 pytest-cov==3.0.0 pytest-html==3.1.1 pytest-metadata==2.0.1 @@ -52,7 +57,6 @@ pytoml==0.1.21 requests==2.27.1 reuse==1.0.0 six==1.16.0 -testfixtures==6.18.5 toml==0.10.2 tomli==2.0.1 tox==3.25.0 diff --git a/mypy.ini b/mypy.ini index 68e0846..a751daf 100644 --- a/mypy.ini +++ b/mypy.ini @@ -47,12 +47,3 @@ warn_return_any = True # Strict Optional checks. # If False, mypy treats None as compatible with every type. (default True) strict_optional = True - -[mypy-py.*] -ignore_missing_imports = True - -[mypy-pytest] -ignore_missing_imports = True - -[mypy-_pytest.*] -ignore_missing_imports = True diff --git a/requirements.in b/requirements.in index 825ab6e..80f3df7 100644 --- a/requirements.in +++ b/requirements.in @@ -4,16 +4,21 @@ black<23 bump2version coverage[toml] dflit -flake8-black -flake8-isort +flake8-bugbear +flake8-comprehensions +flake8-html +flake8-logging-format +flake8-mutable +flake8-pyi fsfe-reuse invoke -mypy==0.931 +isort +mypy==0.971 pip-tools pip>=19.3 pytest-cov pytest-html -pytest~=7.0.1 +pytest~=7.1.2 setuptools>=43 tox-pyenv tox>=3.14.3 diff --git a/requirements.txt b/requirements.txt index 323ecfc..bdc1812 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,13 +5,13 @@ # pip-compile --allow-unsafe --no-emit-index-url # attrs==21.4.0 - # via pytest + # via + # flake8-bugbear + # pytest binaryornot==0.4.4 # via reuse black==22.3.0 - # via - # -r requirements.in - # flake8-black + # via -r requirements.in boolean-py==4.0 # via # license-expression @@ -48,11 +48,22 @@ filelock==3.7.0 # virtualenv flake8==4.0.1 # via - # flake8-black - # flake8-isort -flake8-black==0.3.3 + # flake8-bugbear + # flake8-comprehensions + # flake8-html + # flake8-mutable + # flake8-pyi +flake8-bugbear==22.7.1 + # via -r requirements.in +flake8-comprehensions==3.10.0 + # via -r requirements.in +flake8-html==0.4.2 # via -r requirements.in -flake8-isort==4.1.1 +flake8-logging-format==0.6.0 + # via -r requirements.in +flake8-mutable==1.2.0 + # via -r requirements.in +flake8-pyi==22.7.0 # via -r requirements.in fsfe-reuse==1.0.0 # via -r requirements.in @@ -63,16 +74,18 @@ iniconfig==1.1.1 invoke==1.7.1 # via -r requirements.in isort==5.10.1 - # via flake8-isort + # via -r requirements.in jinja2==3.1.2 - # via reuse + # via + # flake8-html + # reuse license-expression==30.0.0 # via reuse markupsafe==2.1.1 # via jinja2 mccabe==0.6.1 # via flake8 -mypy==0.931 +mypy==0.971 # via -r requirements.in mypy-extensions==0.4.3 # via @@ -103,10 +116,14 @@ py==1.11.0 pycodestyle==2.8.0 # via flake8 pyflakes==2.4.0 - # via flake8 + # via + # flake8 + # flake8-pyi +pygments==2.12.0 + # via flake8-html pyparsing==3.0.9 # via packaging -pytest==7.0.1 +pytest==7.1.2 # via # -r requirements.in # pytest-cov @@ -134,15 +151,12 @@ six==1.16.0 # via # tox # virtualenv -testfixtures==6.18.5 - # via flake8-isort toml==0.10.2 # via tox tomli==2.0.1 # via # black # coverage - # flake8-black # mypy # pep517 # pytest diff --git a/src/pytest_mypy_testing/message.py b/src/pytest_mypy_testing/message.py index 85ab4f5..2997ad7 100644 --- a/src/pytest_mypy_testing/message.py +++ b/src/pytest_mypy_testing/message.py @@ -5,8 +5,9 @@ import dataclasses import enum import os +import pathlib import re -from typing import Optional, Tuple +from typing import Optional, Tuple, Union __all__ = [ @@ -166,7 +167,9 @@ def __str__(self) -> str: return f"{self._prefix} {self.severity.name.lower()}: {self.message}" @classmethod - def from_comment(cls, filename: str, lineno: int, comment: str) -> "Message": + def from_comment( + cls, filename: Union[pathlib.Path, str], lineno: int, comment: str + ) -> "Message": """Create message object from Python *comment*. >>> Message.from_comment("foo.py", 1, "R: foo") @@ -183,7 +186,7 @@ def from_comment(cls, filename: str, lineno: int, comment: str) -> "Message": else: revealed_type = None return Message( - filename, + str(filename), lineno=lineno, colno=colno, severity=Severity.from_string(m.group("severity")), diff --git a/src/pytest_mypy_testing/parser.py b/src/pytest_mypy_testing/parser.py index c0d9900..a38ea01 100644 --- a/src/pytest_mypy_testing/parser.py +++ b/src/pytest_mypy_testing/parser.py @@ -7,9 +7,10 @@ import io import itertools import os +import pathlib import sys import tokenize -from typing import Iterable, Iterator, List, Optional, Set, Tuple +from typing import Iterable, Iterator, List, Optional, Set, Tuple, Union from .message import Message @@ -71,7 +72,7 @@ class MypyTestFile: def iter_comments( - filename: str, token_lists: List[List[tokenize.TokenInfo]] + filename: Union[pathlib.Path, str], token_lists: List[List[tokenize.TokenInfo]] ) -> Iterator[tokenize.TokenInfo]: for toks in token_lists: for tok in toks: @@ -80,7 +81,7 @@ def iter_comments( def iter_mypy_comments( - filename: str, tokens: List[List[tokenize.TokenInfo]] + filename: Union[pathlib.Path, str], tokens: List[List[tokenize.TokenInfo]] ) -> Iterator[Message]: for tok in iter_comments(filename, tokens): try: @@ -103,9 +104,9 @@ def generate_per_line_token_lists(source: str) -> Iterator[List[tokenize.TokenIn i += 1 -def parse_file(filename: str, config) -> MypyTestFile: +def parse_file(filename: Union[os.PathLike, str, pathlib.Path], config) -> MypyTestFile: """Parse *filename* and return information about mypy test cases.""" - filename = os.path.abspath(filename) + filename = pathlib.Path(filename).resolve() with open(filename, "r", encoding="utf-8") as f: source_text = f.read() @@ -113,7 +114,7 @@ def parse_file(filename: str, config) -> MypyTestFile: token_lists = list(generate_per_line_token_lists(source_text)) messages = list(iter_mypy_comments(filename, token_lists)) - tree = ast.parse(source_text, filename=filename) + tree = ast.parse(source_text, filename=str(filename)) if sys.version_info < (3, 8): _add_end_lineno_if_missing(tree, len(source_lines)) @@ -131,7 +132,10 @@ def parse_file(filename: str, config) -> MypyTestFile: ) return MypyTestFile( - filename=filename, source_lines=source_lines, items=items, messages=messages + filename=str(filename), + source_lines=source_lines, + items=items, + messages=messages, ) @@ -140,18 +144,18 @@ def _add_end_lineno_if_missing(tree, line_count: int): prev_node: Optional[ast.AST] = None for node in ast.iter_child_nodes(tree): if prev_node is not None: - setattr(prev_node, "end_lineno", node.lineno) + setattr(prev_node, "end_lineno", node.lineno) # noqa: B010 prev_node = node if prev_node: - setattr(prev_node, "end_lineno", line_count) + setattr(prev_node, "end_lineno", line_count) # noqa: B010 def _find_marks(func_node: ast.FunctionDef) -> Set[str]: - return set( + return { name.split(".", 2)[2] for name, _ in _iter_func_decorators(func_node) if name.startswith("pytest.mark.") - ) + } def _iter_func_decorators(func_node: ast.FunctionDef) -> Iterator[Tuple[str, ast.AST]]: diff --git a/src/pytest_mypy_testing/plugin.py b/src/pytest_mypy_testing/plugin.py index fdd91c2..5f146dc 100644 --- a/src/pytest_mypy_testing/plugin.py +++ b/src/pytest_mypy_testing/plugin.py @@ -2,14 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT import os +import pathlib import tempfile -from typing import Iterable, Iterator, List, NamedTuple, Optional, Tuple +from typing import Iterable, Iterator, List, NamedTuple, Optional, Tuple, Union import mypy.api import pytest from _pytest._code.code import ReprEntry, ReprFileLocation from _pytest.config import Config -from py._path.local import LocalPath from .message import Message, Severity from .output_processing import OutputMismatch, diff_message_sequences @@ -36,6 +36,8 @@ def __init__(self, item, errors: Iterable[OutputMismatch]): class PytestMypyTestItem(pytest.Item): + parent: "PytestMypyFile" + def __init__( self, name: str, @@ -72,7 +74,7 @@ def runtest(self) -> None: if errors: raise MypyAssertionError(item=self, errors=errors) - def reportinfo(self) -> Tuple[str, Optional[int], str]: + def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: return self.parent.fspath, self.mypy_item.lineno, self.name def repr_failure(self, excinfo, style=None): @@ -153,7 +155,8 @@ def run_mypy(self, item: MypyTestItem) -> Tuple[int, List[Message]]: ), ) - def _run_mypy(self, filename: str) -> MypyResult: + def _run_mypy(self, filename: Union[pathlib.Path, os.PathLike, str]) -> MypyResult: + filename = pathlib.Path(filename) with tempfile.TemporaryDirectory(prefix="pytest-mypy-testing-") as tmp_dir_name: mypy_cache_dir = os.path.join(tmp_dir_name, "mypy_cache") @@ -178,9 +181,6 @@ def _run_mypy(self, filename: str) -> MypyResult: lines = (out + err).splitlines() - # for line in lines: - # print("%%%%", line) - file_messages = [ msg for msg in map(Message.from_output, lines) @@ -213,7 +213,7 @@ def _run_mypy(self, filename: str) -> MypyResult: if PYTEST_VERSION_INFO < (7,): - def pytest_collect_file(path: LocalPath, parent): + def pytest_collect_file(path, parent): if path.ext == ".mypy-testing" or _is_pytest_test_file(path, parent): file = PytestMypyFile.from_parent(parent=parent, fspath=path) if file.mypy_file.items: @@ -222,7 +222,7 @@ def pytest_collect_file(path: LocalPath, parent): else: - def pytest_collect_file(file_path, path: LocalPath, parent): # type: ignore + def pytest_collect_file(file_path, path, parent): # type: ignore if path.ext == ".mypy-testing" or _is_pytest_test_file(path, parent): file = PytestMypyFile.from_parent(parent=parent, path=file_path) if file.mypy_file.items: @@ -230,7 +230,7 @@ def pytest_collect_file(file_path, path: LocalPath, parent): # type: ignore return None -def _is_pytest_test_file(path: LocalPath, parent): +def _is_pytest_test_file(path, parent): """Return `True` if *path* is considered to be a pytest test file.""" # Based on _pytest/python.py::pytest_collect_file fn_patterns = parent.config.getini("python_files") + ["__init__.py"] @@ -259,4 +259,4 @@ def _add_reveal_type_to_builtins(): import builtins if not hasattr(builtins, "reveal_type"): - setattr(builtins, "reveal_type", lambda x: x) + setattr(builtins, "reveal_type", lambda x: x) # noqa: B010 diff --git a/tox.ini b/tox.ini index 57d69d9..410d9cb 100644 --- a/tox.ini +++ b/tox.ini @@ -3,10 +3,10 @@ [tox] isolated_build = True envlist = - {py36}-pytest{60,61,62,70}-mypy{0910,0931,0960} - {py37,py38,py39}-pytest{62,70,71}-mypy{0910,0931,0960} - {py310}-pytest{62,71}-mypy{0910,0931,0960} - py-pytest{62,70,71}-mypy{0910,0931,0960} + {py36}-pytest{60,61,62,70}-mypy{0910,0931,0971} + {py37,py38,py39}-pytest{62,70,71}-mypy{0910,0931,0971} + {py310}-pytest{62,71}-mypy{0910,0931,0971} + py-pytest{62,70,71}-mypy{0910,0931,0971} linting [testenv] @@ -29,6 +29,8 @@ deps = mypy0942: mypy==0.942 mypy0950: mypy==0.950 mypy0960: mypy==0.960 + mypy0961: mypy==0.961 + mypy0971: mypy==0.971 setenv = COVERAGE_FILE={toxinidir}/build/{envname}/coverage commands = @@ -37,18 +39,63 @@ commands = --html={toxinidir}/build/{envname}/pytest-report.html \ --junitxml={toxinidir}/build/{envname}/junit.xml -[testenv:linting] +[testenv:black] +basepython = python3.10 +skip_install = True +deps = + -c constraints.txt + black +commands = + python -m black --check --fast --diff {posargs} . + +[testenv:flake8] +basepython = python3.10 +skip_install = True +deps = + -c constraints.txt + flake8 + flake8-bugbear + flake8-comprehensions + flake8-html + flake8-mutable + flake8-pyi + flake8-logging-format +commands = + python -m flake8 {posargs} + +[testenv:isort] +basepython = python3.10 +skip_install = True +deps = + -c constraints.txt + isort +commands = + python -m isort . --check {posargs} + +[testenv:mypy] basepython = python3.10 skip_install = True deps = -cconstraints.txt mypy - flake8 - flake8-isort - flake8-black + pytest commands = mypy src tests - flake8 + +[testenv:linting] +basepython = python3.10 +skip_install = True +deps = + -cconstraints.txt + {[testenv:black]deps} + {[testenv:flake8]deps} + {[testenv:isort]deps} + {[testenv:mypy]deps} +commands = + {[testenv:black]commands} + {[testenv:flake8]commands} + {[testenv:isort]commands} + {[testenv:mypy]commands} [testenv:lock-requirements] basepython = python3.10