Skip to content

Commit fa35f65

Browse files
committed
Fix handling of duplicate args with regard to Python packages
Fixes #4310.
1 parent 176d274 commit fa35f65

File tree

4 files changed

+54
-23
lines changed

4 files changed

+54
-23
lines changed

changelog/4310.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix duplicate collection due to multiple args matching the same packages.

src/_pytest/main.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ def __init__(self, config):
387387
self._initialpaths = frozenset()
388388
# Keep track of any collected nodes in here, so we don't duplicate fixtures
389389
self._node_cache = {}
390+
self._collect_seen_pkgdirs = set()
390391

391392
self.config.pluginmanager.register(self, name="session")
392393

@@ -496,18 +497,19 @@ def _collect(self, arg):
496497
# and stack all Packages found on the way.
497498
# No point in finding packages when collecting doctests
498499
if not self.config.option.doctestmodules:
500+
pm = self.config.pluginmanager
499501
for parent in argpath.parts():
500-
pm = self.config.pluginmanager
501502
if pm._confcutdir and pm._confcutdir.relto(parent):
502503
continue
503504

504505
if parent.isdir():
505506
pkginit = parent.join("__init__.py")
506507
if pkginit.isfile():
508+
self._collect_seen_pkgdirs.add(parent)
507509
if pkginit in self._node_cache:
508510
root = self._node_cache[pkginit][0]
509511
else:
510-
col = root._collectfile(pkginit)
512+
col = root._collectfile(pkginit, handle_dupes=False)
511513
if col:
512514
if isinstance(col[0], Package):
513515
root = col[0]
@@ -529,13 +531,12 @@ def filter_(f):
529531
def filter_(f):
530532
return f.check(file=1)
531533

532-
seen_dirs = set()
533534
for path in argpath.visit(
534535
fil=filter_, rec=self._recurse, bf=True, sort=True
535536
):
536537
dirpath = path.dirpath()
537-
if dirpath not in seen_dirs:
538-
seen_dirs.add(dirpath)
538+
if dirpath not in self._collect_seen_pkgdirs:
539+
self._collect_seen_pkgdirs.add(dirpath)
539540
pkginit = dirpath.join("__init__.py")
540541
if pkginit.exists() and parts(pkginit.strpath).isdisjoint(paths):
541542
for x in root._collectfile(pkginit):
@@ -570,20 +571,20 @@ def filter_(f):
570571
for y in m:
571572
yield y
572573

573-
def _collectfile(self, path):
574+
def _collectfile(self, path, handle_dupes=True):
574575
ihook = self.gethookproxy(path)
575576
if not self.isinitpath(path):
576577
if ihook.pytest_ignore_collect(path=path, config=self.config):
577578
return ()
578579

579-
# Skip duplicate paths.
580-
keepduplicates = self.config.getoption("keepduplicates")
581-
if not keepduplicates:
582-
duplicate_paths = self.config.pluginmanager._duplicatepaths
583-
if path in duplicate_paths:
584-
return ()
585-
else:
586-
duplicate_paths.add(path)
580+
if handle_dupes:
581+
keepduplicates = self.config.getoption("keepduplicates")
582+
if not keepduplicates:
583+
duplicate_paths = self.config.pluginmanager._duplicatepaths
584+
if path in duplicate_paths:
585+
return ()
586+
else:
587+
duplicate_paths.add(path)
587588

588589
return ihook.pytest_collect_file(path=path, parent=self)
589590

src/_pytest/python.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,11 +545,24 @@ def gethookproxy(self, fspath):
545545
proxy = self.config.hook
546546
return proxy
547547

548-
def _collectfile(self, path):
548+
def _collectfile(self, path, handle_dupes=True):
549549
ihook = self.gethookproxy(path)
550550
if not self.isinitpath(path):
551551
if ihook.pytest_ignore_collect(path=path, config=self.config):
552552
return ()
553+
554+
if handle_dupes:
555+
keepduplicates = self.config.getoption("keepduplicates")
556+
if not keepduplicates:
557+
duplicate_paths = self.config.pluginmanager._duplicatepaths
558+
if path in duplicate_paths:
559+
return ()
560+
else:
561+
duplicate_paths.add(path)
562+
563+
if self.fspath == path: # __init__.py
564+
return [self]
565+
553566
return ihook.pytest_collect_file(path=path, parent=self)
554567

555568
def isinitpath(self, path):

testing/test_collection.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -951,19 +951,35 @@ def test_collect_init_tests(testdir):
951951
result = testdir.runpytest(p, "--collect-only")
952952
result.stdout.fnmatch_lines(
953953
[
954-
"*<Module '__init__.py'>",
955-
"*<Function 'test_init'>",
956-
"*<Module 'test_foo.py'>",
957-
"*<Function 'test_foo'>",
954+
"collected 2 items",
955+
"<Package *",
956+
" <Module '__init__.py'>",
957+
" <Function 'test_init'>",
958+
" <Module 'test_foo.py'>",
959+
" <Function 'test_foo'>",
958960
]
959961
)
960962
result = testdir.runpytest("./tests", "--collect-only")
961963
result.stdout.fnmatch_lines(
962964
[
963-
"*<Module '__init__.py'>",
964-
"*<Function 'test_init'>",
965-
"*<Module 'test_foo.py'>",
966-
"*<Function 'test_foo'>",
965+
"collected 2 items",
966+
"<Package *",
967+
" <Module '__init__.py'>",
968+
" <Function 'test_init'>",
969+
" <Module 'test_foo.py'>",
970+
" <Function 'test_foo'>",
971+
]
972+
)
973+
# Ignores duplicates with "." and pkginit (#4310).
974+
result = testdir.runpytest("./tests", ".", "--collect-only")
975+
result.stdout.fnmatch_lines(
976+
[
977+
"collected 2 items",
978+
"<Package *",
979+
" <Module '__init__.py'>",
980+
" <Function 'test_init'>",
981+
" <Module 'test_foo.py'>",
982+
" <Function 'test_foo'>",
967983
]
968984
)
969985
result = testdir.runpytest("./tests/test_foo.py", "--collect-only")

0 commit comments

Comments
 (0)