|
7 | 7 |
|
8 | 8 | import astroid
|
9 | 9 | import pytest
|
| 10 | +from pylint.checkers.variables import VariablesChecker |
10 | 11 |
|
11 | 12 | from ..utils import (
|
12 | 13 | _can_use_fixture,
|
13 | 14 | _is_pytest_fixture,
|
14 | 15 | _is_pytest_mark,
|
15 | 16 | _is_pytest_mark_usefixtures,
|
| 17 | + _is_same_module, |
16 | 18 | )
|
17 | 19 | from . import BasePytestChecker
|
18 |
| -from .types import FixtureDict |
| 20 | +from .types import FixtureDict, replacement_add_message |
19 | 21 |
|
20 | 22 | # TODO: support pytest python_files configuration
|
21 | 23 | FILE_NAME_PATTERNS: tuple[str, ...] = ("test_*.py", "*_test.py")
|
@@ -73,28 +75,38 @@ class FixtureChecker(BasePytestChecker):
|
73 | 75 | }
|
74 | 76 |
|
75 | 77 | # Store all fixtures discovered by pytest session
|
76 |
| - pytest_fixtures: FixtureDict = {} |
| 78 | + _pytest_fixtures: FixtureDict = {} |
77 | 79 | # Stores all used function arguments
|
78 |
| - invoked_with_func_args: set[str] = set() |
| 80 | + _invoked_with_func_args: set[str] = set() |
79 | 81 | # Stores all invoked fixtures through @pytest.mark.usefixture(...)
|
80 |
| - invoked_with_usefixtures: set[str] = set() |
| 82 | + _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 |
81 | 89 |
|
82 | 90 | def close(self):
|
83 | 91 | """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 | + |
84 | 96 | # reset fixture info storage
|
85 |
| - FixtureChecker.pytest_fixtures = {} |
86 |
| - FixtureChecker.invoked_with_func_args = set() |
87 |
| - FixtureChecker.invoked_with_usefixtures = set() |
| 97 | + FixtureChecker._pytest_fixtures = {} |
| 98 | + FixtureChecker._invoked_with_func_args = set() |
| 99 | + FixtureChecker._invoked_with_usefixtures = set() |
88 | 100 |
|
89 | 101 | def visit_module(self, node):
|
90 | 102 | """
|
91 | 103 | - only run once per module
|
92 | 104 | - invoke pytest session to collect available fixtures
|
93 | 105 | - create containers for the module to store args and fixtures
|
94 | 106 | """
|
95 |
| - FixtureChecker.pytest_fixtures = {} |
96 |
| - FixtureChecker.invoked_with_func_args = set() |
97 |
| - FixtureChecker.invoked_with_usefixtures = set() |
| 107 | + FixtureChecker._pytest_fixtures = {} |
| 108 | + FixtureChecker._invoked_with_func_args = set() |
| 109 | + FixtureChecker._invoked_with_usefixtures = set() |
98 | 110 |
|
99 | 111 | is_test_module = False
|
100 | 112 | for pattern in FILE_NAME_PATTERNS:
|
@@ -128,7 +140,7 @@ def visit_module(self, node):
|
128 | 140 | # restore sys.path
|
129 | 141 | sys.path = sys_path
|
130 | 142 |
|
131 |
| - FixtureChecker.pytest_fixtures = fixture_collector.fixtures |
| 143 | + FixtureChecker._pytest_fixtures = fixture_collector.fixtures |
132 | 144 |
|
133 | 145 | legitimate_failure_paths = set(
|
134 | 146 | collection_report.nodeid
|
@@ -212,11 +224,92 @@ def visit_functiondef(self, node):
|
212 | 224 | if _is_pytest_mark_usefixtures(decorator):
|
213 | 225 | # save all visited fixtures
|
214 | 226 | for arg in decorator.args:
|
215 |
| - self.invoked_with_usefixtures.add(arg.value) |
| 227 | + self._invoked_with_usefixtures.add(arg.value) |
216 | 228 | if int(pytest.__version__.split(".")[0]) >= 3 and _is_pytest_fixture(
|
217 | 229 | decorator, fixture=False
|
218 | 230 | ):
|
219 | 231 | # raise deprecated warning for @pytest.yield_fixture
|
220 | 232 | self.add_message("deprecated-pytest-yield-fixture", node=node)
|
221 | 233 | for arg in node.args.args:
|
222 |
| - self.invoked_with_func_args.add(arg.name) |
| 234 | + 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) |
0 commit comments