Skip to content

Commit 59a77a8

Browse files
authored
Merge pull request #16754 from github/tausbn/python-disregard-unused-imports-in-pytest-tests
Python: Disregard unused imports in `pytest` tests
2 parents 9403bf2 + b7b0f84 commit 59a77a8

File tree

4 files changed

+64
-0
lines changed

4 files changed

+64
-0
lines changed

python/ql/src/Imports/UnusedImport.ql

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@
1212

1313
import python
1414
import Variables.Definition
15+
import semmle.python.ApiGraphs
16+
17+
private predicate is_pytest_fixture(Import imp, Variable name) {
18+
exists(Alias a, API::Node pytest_fixture, API::Node decorator |
19+
pytest_fixture = API::moduleImport("pytest").getMember("fixture") and
20+
// The additional `.getReturn()` is to account for the difference between
21+
// ```
22+
// @pytest.fixture
23+
// def foo():
24+
// ...
25+
// ```
26+
// and
27+
// ```
28+
// @pytest.fixture(some, args, here)
29+
// def foo():
30+
// ...
31+
// ```
32+
decorator in [pytest_fixture, pytest_fixture.getReturn()] and
33+
a = imp.getAName() and
34+
a.getAsname().(Name).getVariable() = name and
35+
a.getValue() = decorator.getReturn().getAValueReachableFromSource().asExpr()
36+
)
37+
}
1538

1639
predicate global_name_used(Module m, string name) {
1740
exists(Name u, GlobalVariable v |
@@ -117,6 +140,7 @@ predicate unused_import(Import imp, Variable name) {
117140
not all_not_understood(imp.getEnclosingModule()) and
118141
not imported_module_used_in_doctest(imp) and
119142
not imported_alias_used_in_typehint(imp, name) and
143+
not is_pytest_fixture(imp, name) and
120144
// Only consider import statements that actually point-to something (possibly an unknown module).
121145
// If this is not the case, it's likely that the import statement never gets executed.
122146
imp.getAName().getValue().pointsTo(_)

python/ql/test/query-tests/Imports/unused/UnusedImport.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
| imports_test.py:10:1:10:22 | Import | Import of 'top_level_cycle' is not used. |
66
| imports_test.py:27:1:27:25 | Import | Import of 'func2' is not used. |
77
| imports_test.py:34:1:34:14 | Import | Import of 'module2' is not used. |
8+
| imports_test.py:116:1:116:41 | Import | Import of 'not_a_fixture' is not used. |

python/ql/test/query-tests/Imports/unused/imports_test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,8 @@ def bar(x: Optional['subexpression_parameter_annotation']):
111111

112112
def baz() -> Optional['subexpression_return_type']:
113113
pass
114+
115+
116+
from pytest_fixtures import not_a_fixture # BAD
117+
from pytest_fixtures import fixture, wrapped_fixture # GOOD (pytest fixtures are used implicitly by pytest)
118+
from pytest_fixtures import session_fixture, wrapped_autouse_fixture # GOOD (pytest fixtures are used implicitly by pytest)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def fixture():
6+
pass
7+
8+
def fixture_wrapper():
9+
@pytest.fixture
10+
def delegate():
11+
pass
12+
return delegate
13+
14+
@fixture_wrapper
15+
def wrapped_fixture():
16+
pass
17+
18+
19+
@pytest.fixture(scope='session')
20+
def session_fixture():
21+
pass
22+
23+
def not_a_fixture():
24+
pass
25+
26+
def another_fixture_wrapper():
27+
@pytest.fixture(autouse=True)
28+
def delegate():
29+
pass
30+
return delegate
31+
32+
@another_fixture_wrapper
33+
def wrapped_autouse_fixture():
34+
pass

0 commit comments

Comments
 (0)