Skip to content
Open
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
3 changes: 3 additions & 0 deletions changelog/15187.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Directory collection is now cached when multiple file path arguments share a
parent directory (unless ``--keep-duplicates`` is set), avoiding repeated
directory scans during collection.
12 changes: 11 additions & 1 deletion src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -992,11 +992,21 @@ def collect(self) -> Iterator[nodes.Item | nodes.Collector]:
else:
# For backward compat, files given directly multiple
# times on the command line should not be deduplicated.
handle_dupes = not (
# That intent applies to File nodes (handled in genitems);
# Directory/Package parents must still use the collection
# cache, otherwise N file args from the same directory
# re-walk the directory N times (#15187). Keep the cache
# off under --keep-duplicates so a file named twice still
# gets two Module nodes.
file_arg_leaf = (
len(matchparts) == 1
and isinstance(matchparts[0], Path)
and matchparts[0].is_file()
)
handle_dupes = (
isinstance(matchnode, nodes.Directory)
and not self.config.option.keepduplicates
) or not file_arg_leaf
rep, duplicate = self._collect_one_node(matchnode, handle_dupes)
if not duplicate and not rep.passed:
# Report collection failures here to avoid failing to
Expand Down
29 changes: 29 additions & 0 deletions testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2807,3 +2807,32 @@ def test1(x):
"Duplicates: a",
]
)


def test_directory_collection_cached_for_multiple_file_args(pytester: Pytester) -> None:
"""File args from one directory must not re-collect that directory each time.

See https://github.com/pytest-dev/pytest/issues/15187
"""
calls: dict[str, int] = {}

class Plugin:
def pytest_collect_file(self, file_path: Path) -> None:
calls[file_path.name] = calls.get(file_path.name, 0) + 1

for i in range(5):
pytester.makepyfile(**{f"test_{i:04d}": "def test_x():\n pass\n"})

files = [pytester.path / f"test_{i:04d}.py" for i in range(5)]
result = pytester.runpytest(
"--collect-only",
"-q",
"-p",
"no:cacheprovider",
*map(str, files),
plugins=[Plugin()],
)
assert result.ret == 0
# Each file offered once (plus possibly non-test files; assert the repeated ones)
for i in range(5):
assert calls.get(f"test_{i:04d}.py", 0) == 1, calls
Loading