Skip to content

Commit 2d6d83a

Browse files
Merge pull request #9 from pylint-dev/pre-commit-mypy
Activate mypy in pre-commit
2 parents a444a61 + b8ce4ad commit 2d6d83a

File tree

9 files changed

+105
-47
lines changed

9 files changed

+105
-47
lines changed

.pre-commit-config.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ repos:
4141
- id: rst-directive-colons
4242
- id: rst-inline-touching-normal
4343
- id: text-unicode-replacement-char
44-
# - repo: https://github.com/pre-commit/mirrors-mypy
45-
# rev: v1.6.0
46-
# hooks:
47-
# - id: mypy
44+
- repo: https://github.com/pre-commit/mirrors-mypy
45+
rev: v1.6.0
46+
hooks:
47+
- id: mypy
48+
exclude: "tests/input/"
4849
- repo: local
4950
hooks:
5051
- id: pylint

pylint_pytest/checkers/class_attr_loader.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import astroid
1+
from typing import Optional, Set
2+
3+
from astroid import Assign, Attribute, ClassDef, Name
24
from pylint.interfaces import IAstroidChecker
35

46
from ..utils import _can_use_fixture, _is_class_autouse_fixture
@@ -10,8 +12,8 @@ class ClassAttrLoader(BasePytestChecker):
1012
msgs = {"E6400": ("", "pytest-class-attr-loader", "")}
1113

1214
in_setup = False
13-
request_cls = set()
14-
class_node = None
15+
request_cls: Set[str] = set()
16+
class_node: Optional[ClassDef] = None
1517

1618
def visit_functiondef(self, node):
1719
"""determine if a method is a class setup method"""
@@ -23,12 +25,13 @@ def visit_functiondef(self, node):
2325
self.in_setup = True
2426
self.class_node = node.parent
2527

26-
def visit_assign(self, node):
28+
def visit_assign(self, node: Assign):
2729
"""store the aliases for `cls`"""
2830
if (
2931
self.in_setup
30-
and isinstance(node.value, astroid.Attribute)
32+
and isinstance(node.value, Attribute)
3133
and node.value.attrname == "cls"
34+
and isinstance(node.value.expr, Name)
3235
and node.value.expr.name == "request"
3336
):
3437
# storing the aliases for cls from request.cls
@@ -37,14 +40,15 @@ def visit_assign(self, node):
3740
def visit_assignattr(self, node):
3841
if (
3942
self.in_setup
40-
and isinstance(node.expr, astroid.Name)
43+
and isinstance(node.expr, Name)
4144
and node.expr.name in self.request_cls
45+
and self.class_node is not None
4246
and node.attrname not in self.class_node.locals
4347
):
4448
try:
4549
# find Assign node which contains the source "value"
4650
assign_node = node
47-
while not isinstance(assign_node, astroid.Assign):
51+
while not isinstance(assign_node, Assign):
4852
assign_node = assign_node.parent
4953

5054
# hack class locals

pylint_pytest/checkers/fixture.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import sys
44
from pathlib import Path
5+
from typing import Set, Tuple
56

67
import astroid
78
import pylint
@@ -17,17 +18,19 @@
1718
_is_same_module,
1819
)
1920
from . import BasePytestChecker
21+
from .types import FixtureDict, replacement_add_message
2022

2123
# TODO: support pytest python_files configuration
22-
FILE_NAME_PATTERNS = ("test_*.py", "*_test.py")
24+
FILE_NAME_PATTERNS: Tuple[str, ...] = ("test_*.py", "*_test.py")
2325
ARGUMENT_ARE_KEYWORD_ONLY = (
2426
"https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only"
2527
)
2628

2729

2830
class FixtureCollector:
29-
fixtures = {}
30-
errors = set()
31+
# Same as ``_pytest.fixtures.FixtureManager._arg2fixturedefs``.
32+
fixtures: FixtureDict = {}
33+
errors: Set[pytest.CollectReport] = set()
3134

3235
def pytest_sessionfinish(self, session):
3336
# pylint: disable=protected-access
@@ -73,10 +76,13 @@ class FixtureChecker(BasePytestChecker):
7376
),
7477
}
7578

76-
_pytest_fixtures = {}
77-
_invoked_with_func_args = set()
78-
_invoked_with_usefixtures = set()
79-
_original_add_message = callable
79+
# Store all fixtures discovered by pytest session
80+
_pytest_fixtures: FixtureDict = {}
81+
# Stores all used function arguments
82+
_invoked_with_func_args: Set[str] = set()
83+
# Stores all invoked fixtures through @pytest.mark.usefixture(...)
84+
_invoked_with_usefixtures: Set[str] = set()
85+
_original_add_message = replacement_add_message
8086

8187
def open(self):
8288
# patch VariablesChecker.add_message
@@ -87,7 +93,7 @@ def close(self):
8793
"""restore & reset class attr for testing"""
8894
# restore add_message
8995
VariablesChecker.add_message = FixtureChecker._original_add_message
90-
FixtureChecker._original_add_message = callable
96+
FixtureChecker._original_add_message = replacement_add_message
9197

9298
# reset fixture info storage
9399
FixtureChecker._pytest_fixtures = {}
@@ -100,14 +106,9 @@ def visit_module(self, node):
100106
- invoke pytest session to collect available fixtures
101107
- create containers for the module to store args and fixtures
102108
"""
103-
# storing all fixtures discovered by pytest session
104-
FixtureChecker._pytest_fixtures = {} # Dict[List[_pytest.fixtures.FixtureDef]]
105-
106-
# storing all used function arguments
107-
FixtureChecker._invoked_with_func_args = set() # Set[str]
108-
109-
# storing all invoked fixtures through @pytest.mark.usefixture(...)
110-
FixtureChecker._invoked_with_usefixtures = set() # Set[str]
109+
FixtureChecker._pytest_fixtures = {}
110+
FixtureChecker._invoked_with_func_args = set()
111+
FixtureChecker._invoked_with_usefixtures = set()
111112

112113
is_test_module = False
113114
for pattern in FILE_NAME_PATTERNS:

pylint_pytest/checkers/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import sys
2+
from pprint import pprint
3+
from typing import Any, Dict, List
4+
5+
from _pytest.fixtures import FixtureDef
6+
7+
FixtureDict = Dict[str, List[FixtureDef[Any]]]
8+
9+
10+
def replacement_add_message(*args, **kwargs):
11+
print("Called un-initialized _original_add_message with:", file=sys.stderr)
12+
pprint(args, sys.stderr)
13+
pprint(kwargs, sys.stderr)

pylint_pytest/utils.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,11 @@ def _is_same_module(fixtures, import_node, fixture_name):
108108
try:
109109
for fixture in fixtures[fixture_name]:
110110
for import_from in import_node.root().globals[fixture_name]:
111-
if (
112-
inspect.getmodule(fixture.func).__file__
113-
== import_from.parent.import_module(
114-
import_from.modname, False, import_from.level
115-
).file
116-
):
111+
module = inspect.getmodule(fixture.func)
112+
parent_import = import_from.parent.import_module(
113+
import_from.modname, False, import_from.level
114+
)
115+
if module is not None and module.__file__ == parent_import.file:
117116
return True
118117
except Exception: # pylint: disable=broad-except
119118
pass

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ ignore = [
8989
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
9090
]
9191

92+
# py36, but ruff does not support it :/
93+
target-version = "py37"
94+
9295
[tool.ruff.pydocstyle]
9396
convention = "google"
9497

@@ -110,6 +113,8 @@ convention = "google"
110113

111114
[tool.pylint]
112115

116+
py-version = "3.6"
117+
113118
ignore-paths="tests/input" # Ignore test inputs
114119

115120
load-plugins= [

tests/base_tester.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import os
22
import sys
3+
from abc import ABC
34
from pprint import pprint
5+
from typing import Any, Dict, List
46

57
import astroid
6-
from pylint.testutils import UnittestLinter
8+
from pylint.testutils import MessageTest, UnittestLinter
79

810
try:
911
from pylint.utils import ASTWalker
@@ -15,30 +17,36 @@
1517

1618
import pylint_pytest.checkers.fixture
1719

18-
# XXX: allow all file name
20+
# XXX: allow all file names
1921
pylint_pytest.checkers.fixture.FILE_NAME_PATTERNS = ("*",)
2022

2123

22-
class BasePytestTester:
24+
class BasePytestTester(ABC):
2325
CHECKER_CLASS = BaseChecker
24-
IMPACTED_CHECKER_CLASSES = []
25-
MSG_ID = None
26-
msgs = None
27-
CONFIG = {}
26+
IMPACTED_CHECKER_CLASSES: List[BaseChecker] = []
27+
MSG_ID: str
28+
msgs: List[MessageTest] = []
29+
CONFIG: Dict[str, Any] = {}
30+
31+
def __init_subclass__(cls, **kwargs):
32+
super().__init_subclass__(**kwargs)
33+
if not hasattr(cls, "MSG_ID") or not isinstance(cls.MSG_ID, str) or not cls.MSG_ID:
34+
raise TypeError("Subclasses must define a non-empty MSG_ID of type str")
2835

2936
enable_plugin = True
3037

31-
def run_linter(self, enable_plugin, file_path=None):
38+
def run_linter(self, enable_plugin):
3239
self.enable_plugin = enable_plugin
3340

34-
# pylint: disable=protected-access
35-
if file_path is None:
36-
module = sys._getframe(1).f_code.co_name.replace("test_", "", 1)
37-
file_path = os.path.join(os.getcwd(), "tests", "input", self.MSG_ID, module + ".py")
41+
# pylint: disable-next=protected-access
42+
target_test_file = sys._getframe(1).f_code.co_name.replace("test_", "", 1)
43+
file_path = os.path.join(
44+
os.getcwd(), "tests", "input", self.MSG_ID, target_test_file + ".py"
45+
)
3846

3947
with open(file_path) as fin:
4048
content = fin.read()
41-
module = astroid.parse(content, module_name=module)
49+
module = astroid.parse(content, module_name=target_test_file)
4250
module.file = fin.name
4351

4452
self.walk(module) # run all checkers

tests/base_tester_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pytest
2+
from base_tester import BasePytestTester
3+
4+
# pylint: disable=unused-variable
5+
6+
7+
def test_init_subclass_valid_msg_id():
8+
some_string = "some_string"
9+
10+
class ValidSubclass(BasePytestTester):
11+
MSG_ID = some_string
12+
13+
assert ValidSubclass.MSG_ID == some_string
14+
15+
16+
def test_init_subclass_no_msg_id():
17+
with pytest.raises(TypeError):
18+
19+
class NoMsgIDSubclass(BasePytestTester):
20+
pass
21+
22+
23+
@pytest.mark.parametrize("msg_id", [123, None, ""], ids=lambda x: f"msg_id={x}")
24+
def test_init_subclass_invalid_msg_id_type(msg_id):
25+
with pytest.raises(TypeError):
26+
27+
class Subclass(BasePytestTester):
28+
MSG_ID = msg_id

tests/test_pytest_yield_fixture.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
class TestDeprecatedPytestYieldFixture(BasePytestTester):
77
CHECKER_CLASS = FixtureChecker
8-
IMPACTED_CHECKER_CLASSES = []
98
MSG_ID = "deprecated-pytest-yield-fixture"
109

1110
def test_smoke(self):

0 commit comments

Comments
 (0)