Fix handling of duplicate args with regard to Python packages

Fixes https://github.com/pytest-dev/pytest/issues/4310.
This commit is contained in:
Daniel Hahler 2018-11-06 18:47:19 +01:00
parent 176d27440c
commit fa35f650b5
4 changed files with 54 additions and 23 deletions

View File

@ -0,0 +1 @@
Fix duplicate collection due to multiple args matching the same packages.

View File

@ -387,6 +387,7 @@ class Session(nodes.FSCollector):
self._initialpaths = frozenset() self._initialpaths = frozenset()
# Keep track of any collected nodes in here, so we don't duplicate fixtures # Keep track of any collected nodes in here, so we don't duplicate fixtures
self._node_cache = {} self._node_cache = {}
self._collect_seen_pkgdirs = set()
self.config.pluginmanager.register(self, name="session") self.config.pluginmanager.register(self, name="session")
@ -496,18 +497,19 @@ class Session(nodes.FSCollector):
# and stack all Packages found on the way. # and stack all Packages found on the way.
# No point in finding packages when collecting doctests # No point in finding packages when collecting doctests
if not self.config.option.doctestmodules: if not self.config.option.doctestmodules:
pm = self.config.pluginmanager
for parent in argpath.parts(): for parent in argpath.parts():
pm = self.config.pluginmanager
if pm._confcutdir and pm._confcutdir.relto(parent): if pm._confcutdir and pm._confcutdir.relto(parent):
continue continue
if parent.isdir(): if parent.isdir():
pkginit = parent.join("__init__.py") pkginit = parent.join("__init__.py")
if pkginit.isfile(): if pkginit.isfile():
self._collect_seen_pkgdirs.add(parent)
if pkginit in self._node_cache: if pkginit in self._node_cache:
root = self._node_cache[pkginit][0] root = self._node_cache[pkginit][0]
else: else:
col = root._collectfile(pkginit) col = root._collectfile(pkginit, handle_dupes=False)
if col: if col:
if isinstance(col[0], Package): if isinstance(col[0], Package):
root = col[0] root = col[0]
@ -529,13 +531,12 @@ class Session(nodes.FSCollector):
def filter_(f): def filter_(f):
return f.check(file=1) return f.check(file=1)
seen_dirs = set()
for path in argpath.visit( for path in argpath.visit(
fil=filter_, rec=self._recurse, bf=True, sort=True fil=filter_, rec=self._recurse, bf=True, sort=True
): ):
dirpath = path.dirpath() dirpath = path.dirpath()
if dirpath not in seen_dirs: if dirpath not in self._collect_seen_pkgdirs:
seen_dirs.add(dirpath) self._collect_seen_pkgdirs.add(dirpath)
pkginit = dirpath.join("__init__.py") pkginit = dirpath.join("__init__.py")
if pkginit.exists() and parts(pkginit.strpath).isdisjoint(paths): if pkginit.exists() and parts(pkginit.strpath).isdisjoint(paths):
for x in root._collectfile(pkginit): for x in root._collectfile(pkginit):
@ -570,20 +571,20 @@ class Session(nodes.FSCollector):
for y in m: for y in m:
yield y yield y
def _collectfile(self, path): def _collectfile(self, path, handle_dupes=True):
ihook = self.gethookproxy(path) ihook = self.gethookproxy(path)
if not self.isinitpath(path): if not self.isinitpath(path):
if ihook.pytest_ignore_collect(path=path, config=self.config): if ihook.pytest_ignore_collect(path=path, config=self.config):
return () return ()
# Skip duplicate paths. if handle_dupes:
keepduplicates = self.config.getoption("keepduplicates") keepduplicates = self.config.getoption("keepduplicates")
if not keepduplicates: if not keepduplicates:
duplicate_paths = self.config.pluginmanager._duplicatepaths duplicate_paths = self.config.pluginmanager._duplicatepaths
if path in duplicate_paths: if path in duplicate_paths:
return () return ()
else: else:
duplicate_paths.add(path) duplicate_paths.add(path)
return ihook.pytest_collect_file(path=path, parent=self) return ihook.pytest_collect_file(path=path, parent=self)

View File

@ -545,11 +545,24 @@ class Package(Module):
proxy = self.config.hook proxy = self.config.hook
return proxy return proxy
def _collectfile(self, path): def _collectfile(self, path, handle_dupes=True):
ihook = self.gethookproxy(path) ihook = self.gethookproxy(path)
if not self.isinitpath(path): if not self.isinitpath(path):
if ihook.pytest_ignore_collect(path=path, config=self.config): if ihook.pytest_ignore_collect(path=path, config=self.config):
return () return ()
if handle_dupes:
keepduplicates = self.config.getoption("keepduplicates")
if not keepduplicates:
duplicate_paths = self.config.pluginmanager._duplicatepaths
if path in duplicate_paths:
return ()
else:
duplicate_paths.add(path)
if self.fspath == path: # __init__.py
return [self]
return ihook.pytest_collect_file(path=path, parent=self) return ihook.pytest_collect_file(path=path, parent=self)
def isinitpath(self, path): def isinitpath(self, path):

View File

@ -951,19 +951,35 @@ def test_collect_init_tests(testdir):
result = testdir.runpytest(p, "--collect-only") result = testdir.runpytest(p, "--collect-only")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*<Module '__init__.py'>", "collected 2 items",
"*<Function 'test_init'>", "<Package *",
"*<Module 'test_foo.py'>", " <Module '__init__.py'>",
"*<Function 'test_foo'>", " <Function 'test_init'>",
" <Module 'test_foo.py'>",
" <Function 'test_foo'>",
] ]
) )
result = testdir.runpytest("./tests", "--collect-only") result = testdir.runpytest("./tests", "--collect-only")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*<Module '__init__.py'>", "collected 2 items",
"*<Function 'test_init'>", "<Package *",
"*<Module 'test_foo.py'>", " <Module '__init__.py'>",
"*<Function 'test_foo'>", " <Function 'test_init'>",
" <Module 'test_foo.py'>",
" <Function 'test_foo'>",
]
)
# Ignores duplicates with "." and pkginit (#4310).
result = testdir.runpytest("./tests", ".", "--collect-only")
result.stdout.fnmatch_lines(
[
"collected 2 items",
"<Package *",
" <Module '__init__.py'>",
" <Function 'test_init'>",
" <Module 'test_foo.py'>",
" <Function 'test_foo'>",
] ]
) )
result = testdir.runpytest("./tests/test_foo.py", "--collect-only") result = testdir.runpytest("./tests/test_foo.py", "--collect-only")