Skip to content

Commit 1ca1d86

Browse files
authored
Merge pull request #29 from anis-campos/fix-checkers
See more details in #29
2 parents bde8dde + 46cf450 commit 1ca1d86

File tree

12 files changed

+192
-135
lines changed

12 files changed

+192
-135
lines changed

pylint_pytest/__init__.py

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,21 @@
1-
import glob
2-
import importlib
3-
import inspect
4-
import os
1+
from pylint.checkers.variables import VariablesChecker
2+
from pylint.lint import PyLinter
53

6-
from .checkers import BasePytestChecker
4+
from .checkers.class_attr_loader import ClassAttrLoader
5+
from .checkers.fixture import FixtureChecker
6+
from .checkers.variables import CustomVariablesChecker
77

88

9-
def register(linter):
10-
"""auto discover pylint checker classes"""
11-
dirname = os.path.dirname(__file__)
12-
for module in glob.glob(os.path.join(dirname, "checkers", "*.py")):
13-
# trim file extension
14-
module = os.path.splitext(module)[0]
9+
def register(linter: PyLinter) -> None:
10+
"""Register the checker classes"""
11+
remove_original_variables_checker(linter)
12+
linter.register_checker(CustomVariablesChecker(linter))
13+
linter.register_checker(FixtureChecker(linter))
14+
linter.register_checker(ClassAttrLoader(linter))
1515

16-
# use relative path only
17-
module = module.replace(dirname, "", 1)
1816

19-
# translate file path into module import path
20-
module = module.replace(os.sep, ".")
21-
22-
checker = importlib.import_module(module, package=os.path.basename(dirname))
23-
for attr_name in dir(checker):
24-
attr_val = getattr(checker, attr_name)
25-
if (
26-
attr_val != BasePytestChecker
27-
and inspect.isclass(attr_val)
28-
and issubclass(attr_val, BasePytestChecker)
29-
):
30-
linter.register_checker(attr_val(linter))
17+
def remove_original_variables_checker(linter: PyLinter) -> None:
18+
"""We need to remove VariablesChecker before registering CustomVariablesChecker"""
19+
variable_checkers = linter._checkers[VariablesChecker.name] # pylint: disable=protected-access
20+
for checker in [x for x in variable_checkers if isinstance(x, VariablesChecker)]:
21+
variable_checkers.remove(checker)

pylint_pytest/checkers/fixture.py

Lines changed: 1 addition & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,15 @@
77

88
import astroid
99
import pytest
10-
from pylint.checkers.variables import VariablesChecker
1110

1211
from ..utils import (
1312
_can_use_fixture,
1413
_is_pytest_fixture,
1514
_is_pytest_mark,
1615
_is_pytest_mark_usefixtures,
17-
_is_same_module,
1816
)
1917
from . import BasePytestChecker
20-
from .types import FixtureDict, replacement_add_message
18+
from .types import FixtureDict
2119

2220
# TODO: support pytest python_files configuration
2321
FILE_NAME_PATTERNS: tuple[str, ...] = ("test_*.py", "*_test.py")
@@ -80,19 +78,9 @@ class FixtureChecker(BasePytestChecker):
8078
_invoked_with_func_args: set[str] = set()
8179
# Stores all invoked fixtures through @pytest.mark.usefixture(...)
8280
_invoked_with_usefixtures: set[str] = set()
83-
_original_add_message = replacement_add_message
84-
85-
def open(self):
86-
# patch VariablesChecker.add_message
87-
FixtureChecker._original_add_message = VariablesChecker.add_message
88-
VariablesChecker.add_message = FixtureChecker.patch_add_message
8981

9082
def close(self):
9183
"""restore & reset class attr for testing"""
92-
# restore add_message
93-
VariablesChecker.add_message = FixtureChecker._original_add_message
94-
FixtureChecker._original_add_message = replacement_add_message
95-
9684
# reset fixture info storage
9785
FixtureChecker._pytest_fixtures = {}
9886
FixtureChecker._invoked_with_func_args = set()
@@ -232,84 +220,3 @@ def visit_functiondef(self, node):
232220
self.add_message("deprecated-pytest-yield-fixture", node=node)
233221
for arg in node.args.args:
234222
self._invoked_with_func_args.add(arg.name)
235-
236-
# pylint: disable=bad-staticmethod-argument # The function itself is an if-return logic.
237-
@staticmethod
238-
def patch_add_message(
239-
self, msgid, line=None, node=None, args=None, confidence=None, col_offset=None
240-
):
241-
"""
242-
- intercept and discard unwanted warning messages
243-
"""
244-
# check W0611 unused-import
245-
if msgid == "unused-import":
246-
# actual attribute name is not passed as arg so...dirty hack
247-
# message is usually in the form of '%s imported from %s (as %)'
248-
message_tokens = args.split()
249-
fixture_name = message_tokens[0]
250-
251-
# ignoring 'import %s' message
252-
if message_tokens[0] == "import" and len(message_tokens) == 2:
253-
pass
254-
255-
# fixture is defined in other modules and being imported to
256-
# conftest for pytest magic
257-
elif (
258-
isinstance(node.parent, astroid.Module)
259-
and node.parent.name.split(".")[-1] == "conftest"
260-
and fixture_name in FixtureChecker._pytest_fixtures
261-
):
262-
return
263-
264-
# imported fixture is referenced in test/fixture func
265-
elif (
266-
fixture_name in FixtureChecker._invoked_with_func_args
267-
and fixture_name in FixtureChecker._pytest_fixtures
268-
):
269-
if _is_same_module(
270-
fixtures=FixtureChecker._pytest_fixtures,
271-
import_node=node,
272-
fixture_name=fixture_name,
273-
):
274-
return
275-
276-
# fixture is referenced in @pytest.mark.usefixtures
277-
elif (
278-
fixture_name in FixtureChecker._invoked_with_usefixtures
279-
and fixture_name in FixtureChecker._pytest_fixtures
280-
):
281-
if _is_same_module(
282-
fixtures=FixtureChecker._pytest_fixtures,
283-
import_node=node,
284-
fixture_name=fixture_name,
285-
):
286-
return
287-
288-
# check W0613 unused-argument
289-
if (
290-
msgid == "unused-argument"
291-
and _can_use_fixture(node.parent.parent)
292-
and isinstance(node.parent, astroid.Arguments)
293-
):
294-
if node.name in FixtureChecker._pytest_fixtures:
295-
# argument is used as a fixture
296-
return
297-
298-
fixnames = (
299-
arg.name for arg in node.parent.args if arg.name in FixtureChecker._pytest_fixtures
300-
)
301-
for fixname in fixnames:
302-
if node.name in FixtureChecker._pytest_fixtures[fixname][0].argnames:
303-
# argument is used by a fixture
304-
return
305-
306-
# check W0621 redefined-outer-name
307-
if (
308-
msgid == "redefined-outer-name"
309-
and _can_use_fixture(node.parent.parent)
310-
and isinstance(node.parent, astroid.Arguments)
311-
and node.name in FixtureChecker._pytest_fixtures
312-
):
313-
return
314-
315-
FixtureChecker._original_add_message(self, msgid, line, node, args, confidence, col_offset)

pylint_pytest/checkers/types.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
from __future__ import annotations
22

3-
import sys
4-
from pprint import pprint
53
from typing import Any, Dict, List
64

75
from _pytest.fixtures import FixtureDef
86

97
FixtureDict = Dict[str, List[FixtureDef[Any]]]
10-
11-
12-
def replacement_add_message(*args, **kwargs):
13-
print("Called un-initialized _original_add_message with:", file=sys.stderr)
14-
pprint(args, sys.stderr)
15-
pprint(kwargs, sys.stderr)

pylint_pytest/checkers/variables.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from typing import Any, Optional
2+
3+
from astroid import Arguments, Module
4+
from astroid.nodes.node_ng import NodeNG
5+
from pylint.checkers.variables import VariablesChecker
6+
from pylint.interfaces import Confidence
7+
8+
from pylint_pytest.utils import _can_use_fixture, _is_same_module
9+
10+
from .fixture import FixtureChecker
11+
12+
13+
class CustomVariablesChecker(VariablesChecker):
14+
"""Overrides the default VariablesChecker of pylint to discard unwanted warning messages"""
15+
16+
# pylint: disable=protected-access
17+
# this class needs to access the fixture checker registries
18+
19+
def add_message(
20+
self,
21+
msgid: str,
22+
line: Optional[int] = None,
23+
node: Optional[NodeNG] = None,
24+
args: Any = None,
25+
confidence: Confidence = None,
26+
col_offset: Optional[int] = None,
27+
end_lineno: Optional[int] = None,
28+
end_col_offset: Optional[int] = None,
29+
) -> None:
30+
"""
31+
- intercept and discard unwanted warning messages
32+
"""
33+
# check W0611 unused-import
34+
if msgid == "unused-import":
35+
# actual attribute name is not passed as arg so...dirty hack
36+
# message is usually in the form of '%s imported from %s (as %)'
37+
message_tokens = args.split()
38+
fixture_name = message_tokens[0]
39+
40+
# ignoring 'import %s' message
41+
if message_tokens[0] == "import" and len(message_tokens) == 2:
42+
pass
43+
44+
# fixture is defined in other modules and being imported to
45+
# conftest for pytest magic
46+
elif (
47+
node
48+
and isinstance(node.parent, Module)
49+
and node.parent.name.split(".")[-1] == "conftest"
50+
and fixture_name in FixtureChecker._pytest_fixtures
51+
):
52+
return
53+
54+
# imported fixture is referenced in test/fixture func
55+
elif (
56+
fixture_name in FixtureChecker._invoked_with_func_args
57+
and fixture_name in FixtureChecker._pytest_fixtures
58+
):
59+
if _is_same_module(
60+
fixtures=FixtureChecker._pytest_fixtures,
61+
import_node=node,
62+
fixture_name=fixture_name,
63+
):
64+
return
65+
66+
# fixture is referenced in @pytest.mark.usefixtures
67+
elif (
68+
fixture_name in FixtureChecker._invoked_with_usefixtures
69+
and fixture_name in FixtureChecker._pytest_fixtures
70+
):
71+
if _is_same_module(
72+
fixtures=FixtureChecker._pytest_fixtures,
73+
import_node=node,
74+
fixture_name=fixture_name,
75+
):
76+
return
77+
78+
# check W0613 unused-argument
79+
if (
80+
msgid == "unused-argument"
81+
and node
82+
and _can_use_fixture(node.parent.parent)
83+
and isinstance(node.parent, Arguments)
84+
):
85+
if node.name in FixtureChecker._pytest_fixtures:
86+
# argument is used as a fixture
87+
return
88+
89+
fixnames = (
90+
arg.name for arg in node.parent.args if arg.name in FixtureChecker._pytest_fixtures
91+
)
92+
for fixname in fixnames:
93+
if node.name in FixtureChecker._pytest_fixtures[fixname][0].argnames:
94+
# argument is used by a fixture
95+
return
96+
97+
# check W0621 redefined-outer-name
98+
if (
99+
msgid == "redefined-outer-name"
100+
and node
101+
and _can_use_fixture(node.parent.parent)
102+
and isinstance(node.parent, Arguments)
103+
and node.name in FixtureChecker._pytest_fixtures
104+
):
105+
return
106+
107+
super().add_message(msgid, line, node, args, confidence, col_offset)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import pytest
2+
3+
from other_fixture import other_fixture_not_in_conftest
4+
5+
6+
@pytest.mark.usefixtures("other_fixture_not_in_conftest")
7+
def uses_imported_fixture_with_decorator():
8+
assert True

tests/input/unused-import/module.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import pytest
2+
3+
4+
def test_no_using_module():
5+
assert True
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def other_fixture_not_in_conftest():
6+
return True

tests/test_pylint_integration.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
The tests in this file shall detect any error related to actual execution of pylint, while the
3+
other test are more unit tests that focuses on the checkers behaviour.
4+
5+
Notes:
6+
Tests here are voluntarily minimalistic, the goal is not to test pylint, it is only checking
7+
that pylint_pytest integrates just fine
8+
"""
9+
import subprocess
10+
11+
12+
def test_simple_process():
13+
result = subprocess.run(
14+
["pylint", "--load-plugins", "pylint_pytest", "tests"],
15+
capture_output=True,
16+
check=False,
17+
)
18+
# then no error
19+
assert not result.stderr
20+
21+
22+
def test_multi_process():
23+
result = subprocess.run(
24+
["pylint", "--load-plugins", "pylint_pytest", "-j", "2", "tests"],
25+
capture_output=True,
26+
check=False,
27+
)
28+
# then no error
29+
assert not result.stderr

tests/test_redefined_outer_name.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import pytest
22
from base_tester import BasePytestTester
3-
from pylint.checkers.variables import VariablesChecker
43

54
from pylint_pytest.checkers.fixture import FixtureChecker
5+
from pylint_pytest.checkers.variables import CustomVariablesChecker
66

77

88
class TestRedefinedOuterName(BasePytestTester):
99
CHECKER_CLASS = FixtureChecker
10-
IMPACTED_CHECKER_CLASSES = [VariablesChecker]
10+
IMPACTED_CHECKER_CLASSES = [CustomVariablesChecker]
1111
MSG_ID = "redefined-outer-name"
1212

1313
@pytest.mark.parametrize("enable_plugin", [True, False])

tests/test_regression.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import pytest
22
from base_tester import BasePytestTester
3-
from pylint.checkers.variables import VariablesChecker
43

54
from pylint_pytest.checkers.fixture import FixtureChecker
5+
from pylint_pytest.checkers.variables import CustomVariablesChecker
66

77

88
class TestRegression(BasePytestTester):
99
"""Covering some behaviors that shouldn't get impacted by the plugin"""
1010

1111
CHECKER_CLASS = FixtureChecker
12-
IMPACTED_CHECKER_CLASSES = [VariablesChecker]
12+
IMPACTED_CHECKER_CLASSES = [CustomVariablesChecker]
1313
MSG_ID = "regression"
1414

1515
@pytest.mark.parametrize("enable_plugin", [True, False])

tests/test_unused_argument.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import pytest
22
from base_tester import BasePytestTester
3-
from pylint.checkers.variables import VariablesChecker
43

54
from pylint_pytest.checkers.fixture import FixtureChecker
5+
from pylint_pytest.checkers.variables import CustomVariablesChecker
66

77

88
class TestUnusedArgument(BasePytestTester):
99
CHECKER_CLASS = FixtureChecker
10-
IMPACTED_CHECKER_CLASSES = [VariablesChecker]
10+
IMPACTED_CHECKER_CLASSES = [CustomVariablesChecker]
1111
MSG_ID = "unused-argument"
1212

1313
@pytest.mark.parametrize("enable_plugin", [True, False])

0 commit comments

Comments
 (0)