Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deps/tests/bump/main/output/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
click==8.3.1
click==8.3.2
83 changes: 83 additions & 0 deletions pytest/fixture_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import ast
import sys


class FixtureNameChecker(ast.NodeVisitor):
def __init__(self, filename, source_lines):
self.filename = filename
self.source_lines = source_lines
self.issues = []
self.pytest_aliases: set[str] = set() # names where name.fixture is the decorator
self.fixture_aliases: set[str] = set() # names that *are* pytest.fixture

def visit_Import(self, node):
for alias in node.names:
if alias.name == "pytest":
self.pytest_aliases.add(alias.asname or alias.name)
self.generic_visit(node)

def visit_ImportFrom(self, node):
if node.module == "pytest":
for alias in node.names:
if alias.name == "fixture":
self.fixture_aliases.add(alias.asname or alias.name)
self.generic_visit(node)

def _is_fixture_decorator(self, decorator):
# Unwrap call: @pytest.fixture() or @pytest.fixture(autouse=True)
if isinstance(decorator, ast.Call):
decorator = decorator.func
# @pytest.fixture or @pt.fixture
if (
isinstance(decorator, ast.Attribute)
and decorator.attr == "fixture"
and isinstance(decorator.value, ast.Name)
and decorator.value.id in self.pytest_aliases
):
return True
# @fixture or @fx
if isinstance(decorator, ast.Name) and decorator.id in self.fixture_aliases:
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: you can return isinstance(...) and ...

return False

def _check_function(self, node):
if node.name.startswith("_") and any(
self._is_fixture_decorator(d) for d in node.decorator_list
):
# Find the name's column in the source line — more reliable than +4
# and handles 'async def' correctly (col_offset points to 'async').
line = self.source_lines[node.lineno - 1]
name_col = line.index(node.name, node.col_offset)
self.issues.append((node.lineno, name_col, node.name))

def visit_FunctionDef(self, node):
self._check_function(node)
self.generic_visit(node)

def visit_AsyncFunctionDef(self, node):
self._check_function(node)
self.generic_visit(node)


def main(filenames):
exit_status = 0
for filename in filenames:
with open(filename, "rb") as f:
source = f.read()
try:
tree = ast.parse(source, filename=filename)
except SyntaxError:
continue
source_lines = source.decode("utf-8", errors="replace").splitlines()
checker = FixtureNameChecker(filename, source_lines)
checker.visit(tree)
for lineno, col, name in checker.issues:
print(
f"{filename}:{lineno}:{col}: no need to make things look private in tests: '{name}'"
)
exit_status = 99
sys.exit(exit_status)


if __name__ == "__main__":
main(sys.argv[1:])
6 changes: 6 additions & 0 deletions pytest/ick.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ impl = "python"
scope = "file"
inputs = ["*.py"]
deps = ["fixit", "libcst"]

[[rule]]
name = "fixture_names"
impl = "python"
scope = "file"
inputs = ["tests/*.py"]
57 changes: 57 additions & 0 deletions pytest/tests/fixture_names/main/input/test_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import pytest
from pytest import fixture
import pytest as pt
from pytest import fixture as fx


# Bad: leading underscore with @pytest.fixture (attribute form)
@pytest.fixture
def _plain_fixture():
pass


# Bad: called with no args
@pytest.fixture()
def _called_fixture():
pass


# Bad: called with autouse=True
@pytest.fixture(autouse=True)
def _autouse_fixture():
pass


# Bad: from-import form
@fixture
def _direct_fixture():
pass


# Bad: aliased module import
@pt.fixture
def _aliased_module_fixture():
pass


# Bad: aliased direct import
@fx
def _aliased_direct_fixture():
pass


# Bad: async fixture
@pytest.fixture
async def _async_fixture():
pass


# Good: no underscore
@pytest.fixture
def plain_fixture():
pass


# Good: underscore but no fixture decorator
def _helper():
pass
7 changes: 7 additions & 0 deletions pytest/tests/fixture_names/main/output/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
test_example.py:9:4: no need to make things look private in tests: '_plain_fixture'
test_example.py:15:4: no need to make things look private in tests: '_called_fixture'
test_example.py:21:4: no need to make things look private in tests: '_autouse_fixture'
test_example.py:27:4: no need to make things look private in tests: '_direct_fixture'
test_example.py:33:4: no need to make things look private in tests: '_aliased_module_fixture'
test_example.py:39:4: no need to make things look private in tests: '_aliased_direct_fixture'
test_example.py:45:10: no need to make things look private in tests: '_async_fixture'
Loading