Skip to content

Commit

Permalink
usefixtures validator: ignore self|cls on class tests
Browse files Browse the repository at this point in the history
  • Loading branch information
micheller committed Nov 30, 2020
1 parent 2d0b9f2 commit 9ba2460
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 6 deletions.
53 changes: 47 additions & 6 deletions flake8_fine_pytest/watchers/usefixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
from flake8_fine_pytest.watchers.base import BaseWatcher


def is_static_method(node: ast.FunctionDef) -> bool:
for decorator in node.decorator_list:
if isinstance(decorator, ast.Name) and decorator.id == 'staticmethod':
return True


class UsefixturesWatcher(BaseWatcher):
error_template = (
'FP009 {test_name} should use fixtures as follows: '
Expand All @@ -18,23 +24,58 @@ def _should_run(self) -> bool:
return self.options.force_usefixtures and super()._should_run()

def _validate_usefixtures_used_where_possible(self, tree: ast.AST) -> None:
for node in ast.walk(tree):
if not self._should_check_node(node):
continue

fixture_names = self._get_unreferenced_fixture_names(node) # type: ignore
for test_node, is_class_member in self._iterate_over_test_function_definitions(tree):
fixture_names = self._get_unreferenced_fixture_names(
test_node, is_class_member,
)
if fixture_names:
self._add_usefixtures_error(node, fixture_names) # type: ignore
self._add_usefixtures_error(test_node, fixture_names)

def _iterate_over_test_function_definitions(
self, tree: ast.AST,
) -> typing.Iterator[typing.Tuple[ast.FunctionDef, bool]]:
"""
Returns any FunctionDef that looks like a test.
As pytest only discovers first-level-of-nesting tests, we only yield
top-level function definitions and top-level classes method definitions.
"""
for node in ast.iter_child_nodes(self.tree):
if self._should_check_node(node):
yield node, False # type: ignore

elif(
isinstance(node, ast.ClassDef)
and node.name.startswith(('Test', 'test'))
):
yield from self._iterate_over_test_class_nodes(node)

def _iterate_over_test_class_nodes(
self, class_node: ast.AST,
) -> typing.Iterator[typing.Tuple[ast.FunctionDef, bool]]:
for node in ast.iter_child_nodes(class_node):
if self._should_check_node(node):
yield node, True # type: ignore

def _get_unreferenced_fixture_names(
self,
function_node: ast.FunctionDef,
is_class_method: bool,
) -> typing.List[str]:
referenced_variable_names = {
node.id
for node in ast.walk(function_node)
if isinstance(node, ast.Name)
}

if is_class_method and not is_static_method(function_node):
try:
# `self` or `cls`, or whatever it happens to be called.
referenced_variable_names.add(function_node.args.args[0].arg)
except IndexError:
# wrong class method definition. Skip silently.
pass

test_fixture_names = {arg.arg for arg in function_node.args.args}

return sorted(test_fixture_names - referenced_variable_names)
Expand Down
14 changes: 14 additions & 0 deletions tests/test_files/test_class_with_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class TestClass:
def test_one(self, caplog, capsys):
assert caplog

@classmethod
def test_two(cls, caplog):
assert caplog

def three(self, caplog):
pass

# this method's signature is broken on purpose.
def four():
pass

0 comments on commit 9ba2460

Please sign in to comment.