PY-71370: Resolve any suitable fixture candidate if pytest_plugins cannot be parsed

This bug was introduced during a refactoring of fixture support.
While the correct order of resolution was maintained, it relied on the assumption that pytest_plugins could be statically analyzed.

Consider the following example:
```python
import os
from glob import iglob

DIR_PATH = os.path.dirname(os.path.abspath(__file__))

def create_pytest_plugins():
    # Dynamically resolves fixture plugin names by scanning the fixtures directory
    fixture_names = _make_fixture_names("tests/utils/fixtures/**/*.py")
    return fixture_names

def _make_fixture_names(fixture_path_pattern: str):
    os.chdir(f"{DIR_PATH}/../../")
    return [
        _make_fixture_name(fixture_path)
        for fixture_path in iglob(fixture_path_pattern, recursive=True)
        if "__" not in fixture_path
    ]

def _make_fixture_name(fixture_path: str) -> str:
    return fixture_path.replace("/", ".").replace(".py", "")

pytest_plugins = create_pytest_plugins()
```
In such cases, where pytest_plugins is resolved dynamically and cannot be parsed statically, it is preferable to fall back to resolving any other suitable fixture found in the project, rather than skipping resolution altogether.


(cherry picked from commit 2d9a5dd6bd34d1c06d47b3587cd365696642ccd7)

IJ-MR-167570

GitOrigin-RevId: 709f080d6c23d1f76ad397cb6363e74623758cfd
This commit is contained in:
Ilia Zakoulov
2025-06-30 21:48:16 +02:00
committed by intellij-monorepo-bot
parent 3e84f70fa3
commit a73bbf2a4d
5 changed files with 32 additions and 1 deletions

View File

@@ -274,7 +274,12 @@ private fun getFixtureFromPytestPlugins(targetFile: PyFile, fixtureCandidates: L
is PyParenthesizedExpression -> assignedValue.children.find { it is PyTupleExpression }?.let { tuple ->
(tuple as PyTupleExpression).elements.mapNotNull { resolve(it) }
} ?: emptyList()
else -> emptyList()
else -> {
// `pytest_plugins` is not parsable, most likely looks like `pytest_plugins = create_pytest_plugins()`
// In that case it is better to return any suitable fixtureCandidate
val candidate = fixtureCandidates.firstOrNull() ?: return null
return NamedFixtureLink(candidate, null)
}
}
if (fixtures.isEmpty()) return null

View File

@@ -0,0 +1,9 @@
def calculate_fixtures():
# Imitate custom logic around collecting folders.
# For the real-world examples see PY-71370
fixtures_folder = "fixtures"
subdirectories = ["first", "second"]
return [f"{fixtures_folder}.{subdir}" for subdir in subdirectories]
pytest_plugins = calculate_fixtures()

View File

@@ -0,0 +1,5 @@
import pytest
@pytest.fixture
def first():
return 1

View File

@@ -0,0 +1,5 @@
import pytest
def test_first(fi<caret>rst):
assert first == 1

View File

@@ -70,6 +70,7 @@ class PyTestFixtureResolvingTest : PyTestCase() {
const val PYTEST_PLUGINS_FIXTURES_AS_STR_DIR = "/pytest_plugins_as_str"
const val PYTEST_PLUGINS_FIXTURES_AS_TUPLE_DIR = "/pytest_plugins_as_tuple"
const val PYTEST_PLUGINS_FIXTURES_AS_REF_DIR = "/pytest_plugins_as_ref"
const val PYTEST_PLUGINS_FIXTURES_NOT_PARSABLE_DIR = "/pytest_plugins_not_parsable"
const val PYTEST_PLUGINS_FIXTURES = "fixtures"
const val PYTEST_PLUGINS_FIXTURES_FIRST = "first.py"
const val PYTEST_PLUGINS_FIXTURES_SECOND = "second.py"
@@ -79,6 +80,7 @@ class PyTestFixtureResolvingTest : PyTestCase() {
const val PYTEST_PLUGINS_FIXTURES_AS_TUPLE_SECOND_TEST = "/test_pytest_plugins_as_tuple_second.py"
const val PYTEST_PLUGINS_FIXTURES_AS_STR_TEST = "/test_pytest_plugins_as_str.py"
const val PYTEST_PLUGINS_FIXTURES_AS_REF_TEST = "/test_pytest_plugins_as_ref.py"
const val PYTEST_PLUGINS_FIXTURES_NOT_PARSABLE_TEST = "/test_pytest_plugins_not_parsable.py"
const val IMPORT_WITH_WILDCARD_DIR_NAME = "testImportWithWildcard"
const val IMPORT_WITH_WILDCARD_DIR = "/$IMPORT_WITH_WILDCARD_DIR_NAME"
@@ -304,6 +306,11 @@ class PyTestFixtureResolvingTest : PyTestCase() {
assertCorrectFile(testDir, PYTEST_PLUGINS_FIXTURES_AS_REF_TEST, PYTEST_PLUGINS_FIXTURES_FIRST, PYTEST_PLUGINS_FIXTURES)
}
fun testPytestPluginsFixtureNotParsable() {
val testDir = PYTEST_PLUGINS_FIXTURES_DIR + PYTEST_PLUGINS_FIXTURES_NOT_PARSABLE_DIR
assertCorrectFile(testDir, PYTEST_PLUGINS_FIXTURES_NOT_PARSABLE_TEST, PYTEST_PLUGINS_FIXTURES_FIRST, PYTEST_PLUGINS_FIXTURES)
}
fun testImportWithWildCardFromInit() {
val testDir = IMPORT_WITH_WILDCARD_DIR + IMPORT_WITH_WILDCARD_FROM_INIT_DIR
assertCorrectFile(testDir, IMPORT_WITH_WILDCARD_TEST_FILE, IMPORT_WITH_WILDCARD_FIXTURES_FILE, IMPORT_WITH_WILDCARD_FIXTURES_DIR)