Skip to content

Commit 7a3ec7d

Browse files
committed
Refactor Session._initialparts to have a more explicit type
Previously, _initialparts was a list whose first item was a `py.path.local` and the rest were `str`s. This is not something that mypy is capable of modeling. The type `List[Union[str, py.path.local]]` is too broad and would require asserts for every access. Instead, make each item a `Tuple[py.path.local, List[str]]`. This way the structure is clear and the types are accurate.
1 parent 10e243d commit 7a3ec7d

File tree

2 files changed

+26
-28
lines changed

2 files changed

+26
-28
lines changed

src/_pytest/main.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Dict
99
from typing import FrozenSet
1010
from typing import List
11+
from typing import Tuple
1112

1213
import attr
1314
import py
@@ -482,13 +483,13 @@ def _perform_collect(self, args, genitems):
482483
self.trace("perform_collect", self, args)
483484
self.trace.root.indent += 1
484485
self._notfound = []
485-
initialpaths = []
486-
self._initialparts = []
486+
initialpaths = [] # type: List[py.path.local]
487+
self._initialparts = [] # type: List[Tuple[py.path.local, List[str]]]
487488
self.items = items = []
488489
for arg in args:
489-
parts = self._parsearg(arg)
490-
self._initialparts.append(parts)
491-
initialpaths.append(parts[0])
490+
fspath, parts = self._parsearg(arg)
491+
self._initialparts.append((fspath, parts))
492+
initialpaths.append(fspath)
492493
self._initialpaths = frozenset(initialpaths)
493494
rep = collect_one_node(self)
494495
self.ihook.pytest_collectreport(report=rep)
@@ -508,25 +509,22 @@ def _perform_collect(self, args, genitems):
508509
return items
509510

510511
def collect(self):
511-
for initialpart in self._initialparts:
512-
self.trace("processing argument", initialpart)
512+
for fspath, parts in self._initialparts:
513+
self.trace("processing argument", (fspath, parts))
513514
self.trace.root.indent += 1
514515
try:
515-
yield from self._collect(initialpart)
516+
yield from self._collect(fspath, parts)
516517
except NoMatch:
517-
report_arg = "::".join(map(str, initialpart))
518+
report_arg = "::".join(part for part in [str(fspath), *parts])
518519
# we are inside a make_report hook so
519520
# we cannot directly pass through the exception
520521
self._notfound.append((report_arg, sys.exc_info()[1]))
521522

522523
self.trace.root.indent -= 1
523524

524-
def _collect(self, arg):
525+
def _collect(self, argpath, names):
525526
from _pytest.python import Package
526527

527-
names = arg[:]
528-
argpath = names.pop(0)
529-
530528
# Start with a Session root, and delve to argpath item (dir or file)
531529
# and stack all Packages found on the way.
532530
# No point in finding packages when collecting doctests
@@ -550,7 +548,7 @@ def _collect(self, arg):
550548
# If it's a directory argument, recurse and look for any Subpackages.
551549
# Let the Package collector deal with subnodes, don't collect here.
552550
if argpath.check(dir=1):
553-
assert not names, "invalid arg {!r}".format(arg)
551+
assert not names, "invalid arg {!r}".format((argpath, names))
554552

555553
seen_dirs = set()
556554
for path in argpath.visit(
@@ -660,19 +658,19 @@ def _tryconvertpyarg(self, x):
660658

661659
def _parsearg(self, arg):
662660
""" return (fspath, names) tuple after checking the file exists. """
663-
parts = str(arg).split("::")
661+
strpath, *parts = str(arg).split("::")
664662
if self.config.option.pyargs:
665-
parts[0] = self._tryconvertpyarg(parts[0])
666-
relpath = parts[0].replace("/", os.sep)
667-
path = self.config.invocation_dir.join(relpath, abs=True)
668-
if not path.check():
663+
strpath = self._tryconvertpyarg(strpath)
664+
relpath = strpath.replace("/", os.sep)
665+
fspath = self.config.invocation_dir.join(relpath, abs=True)
666+
if not fspath.check():
669667
if self.config.option.pyargs:
670668
raise UsageError(
671669
"file or package not found: " + arg + " (missing __init__.py?)"
672670
)
673671
raise UsageError("file not found: " + arg)
674-
parts[0] = path.realpath()
675-
return parts
672+
fspath = fspath.realpath()
673+
return (fspath, parts)
676674

677675
def matchnodes(self, matching, names):
678676
self.trace("matchnodes", matching, names)

testing/test_collection.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ def pytest_collect_file(path, parent):
438438

439439

440440
class TestSession:
441-
def test_parsearg(self, testdir):
441+
def test_parsearg(self, testdir) -> None:
442442
p = testdir.makepyfile("def test_func(): pass")
443443
subdir = testdir.mkdir("sub")
444444
subdir.ensure("__init__.py")
@@ -448,14 +448,14 @@ def test_parsearg(self, testdir):
448448
config = testdir.parseconfig(p.basename)
449449
rcol = Session(config=config)
450450
assert rcol.fspath == subdir
451-
parts = rcol._parsearg(p.basename)
451+
fspath, parts = rcol._parsearg(p.basename)
452452

453-
assert parts[0] == target
453+
assert fspath == target
454+
assert len(parts) == 0
455+
fspath, parts = rcol._parsearg(p.basename + "::test_func")
456+
assert fspath == target
457+
assert parts[0] == "test_func"
454458
assert len(parts) == 1
455-
parts = rcol._parsearg(p.basename + "::test_func")
456-
assert parts[0] == target
457-
assert parts[1] == "test_func"
458-
assert len(parts) == 2
459459

460460
def test_collect_topdir(self, testdir):
461461
p = testdir.makepyfile("def test_func(): pass")

0 commit comments

Comments
 (0)