Skip to content

Commit 5e0d117

Browse files
committed
main: model the result of resolve_collection_arguments as a dataclass
In preparation of adding more info to it.
1 parent ff4c3b2 commit 5e0d117

File tree

2 files changed

+86
-36
lines changed

2 files changed

+86
-36
lines changed

src/_pytest/main.py

+36-14
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ def __init__(self, config: Config) -> None:
564564
self._initialpaths: FrozenSet[Path] = frozenset()
565565
self._initialpaths_with_parents: FrozenSet[Path] = frozenset()
566566
self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []
567-
self._initial_parts: List[Tuple[Path, List[str]]] = []
567+
self._initial_parts: List[CollectionArgument] = []
568568
self._collection_cache: Dict[nodes.Collector, CollectReport] = {}
569569
self.items: List[nodes.Item] = []
570570

@@ -770,15 +770,15 @@ def perform_collect(
770770
initialpaths: List[Path] = []
771771
initialpaths_with_parents: List[Path] = []
772772
for arg in args:
773-
fspath, parts = resolve_collection_argument(
773+
collection_argument = resolve_collection_argument(
774774
self.config.invocation_params.dir,
775775
arg,
776776
as_pypath=self.config.option.pyargs,
777777
)
778-
self._initial_parts.append((fspath, parts))
779-
initialpaths.append(fspath)
780-
initialpaths_with_parents.append(fspath)
781-
initialpaths_with_parents.extend(fspath.parents)
778+
self._initial_parts.append(collection_argument)
779+
initialpaths.append(collection_argument.path)
780+
initialpaths_with_parents.append(collection_argument.path)
781+
initialpaths_with_parents.extend(collection_argument.path.parents)
782782
self._initialpaths = frozenset(initialpaths)
783783
self._initialpaths_with_parents = frozenset(initialpaths_with_parents)
784784

@@ -840,10 +840,13 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
840840

841841
pm = self.config.pluginmanager
842842

843-
for argpath, names in self._initial_parts:
844-
self.trace("processing argument", (argpath, names))
843+
for collection_argument in self._initial_parts:
844+
self.trace("processing argument", collection_argument)
845845
self.trace.root.indent += 1
846846

847+
argpath = collection_argument.path
848+
names = collection_argument.parts
849+
847850
# resolve_collection_argument() ensures this.
848851
if argpath.is_dir():
849852
assert not names, f"invalid arg {(argpath, names)!r}"
@@ -862,7 +865,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
862865
notfound_collectors = []
863866
work: List[
864867
Tuple[Union[nodes.Collector, nodes.Item], List[Union[Path, str]]]
865-
] = [(self, paths + names)]
868+
] = [(self, [*paths, *names])]
866869
while work:
867870
matchnode, matchparts = work.pop()
868871

@@ -971,27 +974,43 @@ def search_pypath(module_name: str) -> str:
971974
return spec.origin
972975

973976

977+
@dataclasses.dataclass(frozen=True)
978+
class CollectionArgument:
979+
"""A resolved collection argument."""
980+
981+
path: Path
982+
parts: Sequence[str]
983+
984+
974985
def resolve_collection_argument(
975986
invocation_path: Path, arg: str, *, as_pypath: bool = False
976-
) -> Tuple[Path, List[str]]:
987+
) -> CollectionArgument:
977988
"""Parse path arguments optionally containing selection parts and return (fspath, names).
978989
979990
Command-line arguments can point to files and/or directories, and optionally contain
980991
parts for specific tests selection, for example:
981992
982993
"pkg/tests/test_foo.py::TestClass::test_foo"
983994
984-
This function ensures the path exists, and returns a tuple:
995+
This function ensures the path exists, and returns a resolved `CollectionArgument`:
985996
986-
(Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
997+
CollectionArgument(
998+
path=Path("/full/path/to/pkg/tests/test_foo.py"),
999+
parts=["TestClass", "test_foo"],
1000+
)
9871001
9881002
When as_pypath is True, expects that the command-line argument actually contains
9891003
module paths instead of file-system paths:
9901004
9911005
"pkg.tests.test_foo::TestClass::test_foo"
9921006
9931007
In which case we search sys.path for a matching module, and then return the *path* to the
994-
found module.
1008+
found module, which may look like this:
1009+
1010+
CollectionArgument(
1011+
path=Path("/home/u/myvenv/lib/site-packages/pkg/tests/test_foo.py"),
1012+
parts=["TestClass", "test_foo"],
1013+
)
9951014
9961015
If the path doesn't exist, raise UsageError.
9971016
If the path is a directory and selection parts are present, raise UsageError.
@@ -1018,4 +1037,7 @@ def resolve_collection_argument(
10181037
else "directory argument cannot contain :: selection parts: {arg}"
10191038
)
10201039
raise UsageError(msg.format(arg=arg))
1021-
return fspath, parts
1040+
return CollectionArgument(
1041+
path=fspath,
1042+
parts=parts,
1043+
)

testing/test_main.py

+50-22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from _pytest.config import ExitCode
1010
from _pytest.config import UsageError
11+
from _pytest.main import CollectionArgument
1112
from _pytest.main import resolve_collection_argument
1213
from _pytest.main import validate_basetemp
1314
from _pytest.pytester import Pytester
@@ -133,26 +134,38 @@ def invocation_path(self, pytester: Pytester) -> Path:
133134

134135
def test_file(self, invocation_path: Path) -> None:
135136
"""File and parts."""
136-
assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == (
137-
invocation_path / "src/pkg/test.py",
138-
[],
137+
assert resolve_collection_argument(
138+
invocation_path, "src/pkg/test.py"
139+
) == CollectionArgument(
140+
path=invocation_path / "src/pkg/test.py",
141+
parts=[],
139142
)
140-
assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == (
141-
invocation_path / "src/pkg/test.py",
142-
[""],
143+
assert resolve_collection_argument(
144+
invocation_path, "src/pkg/test.py::"
145+
) == CollectionArgument(
146+
path=invocation_path / "src/pkg/test.py",
147+
parts=[""],
143148
)
144149
assert resolve_collection_argument(
145150
invocation_path, "src/pkg/test.py::foo::bar"
146-
) == (invocation_path / "src/pkg/test.py", ["foo", "bar"])
151+
) == CollectionArgument(
152+
path=invocation_path / "src/pkg/test.py",
153+
parts=["foo", "bar"],
154+
)
147155
assert resolve_collection_argument(
148156
invocation_path, "src/pkg/test.py::foo::bar::"
149-
) == (invocation_path / "src/pkg/test.py", ["foo", "bar", ""])
157+
) == CollectionArgument(
158+
path=invocation_path / "src/pkg/test.py",
159+
parts=["foo", "bar", ""],
160+
)
150161

151162
def test_dir(self, invocation_path: Path) -> None:
152163
"""Directory and parts."""
153-
assert resolve_collection_argument(invocation_path, "src/pkg") == (
154-
invocation_path / "src/pkg",
155-
[],
164+
assert resolve_collection_argument(
165+
invocation_path, "src/pkg"
166+
) == CollectionArgument(
167+
path=invocation_path / "src/pkg",
168+
parts=[],
156169
)
157170

158171
with pytest.raises(
@@ -169,13 +182,21 @@ def test_pypath(self, invocation_path: Path) -> None:
169182
"""Dotted name and parts."""
170183
assert resolve_collection_argument(
171184
invocation_path, "pkg.test", as_pypath=True
172-
) == (invocation_path / "src/pkg/test.py", [])
185+
) == CollectionArgument(
186+
path=invocation_path / "src/pkg/test.py",
187+
parts=[],
188+
)
173189
assert resolve_collection_argument(
174190
invocation_path, "pkg.test::foo::bar", as_pypath=True
175-
) == (invocation_path / "src/pkg/test.py", ["foo", "bar"])
176-
assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == (
177-
invocation_path / "src/pkg",
178-
[],
191+
) == CollectionArgument(
192+
path=invocation_path / "src/pkg/test.py",
193+
parts=["foo", "bar"],
194+
)
195+
assert resolve_collection_argument(
196+
invocation_path, "pkg", as_pypath=True
197+
) == CollectionArgument(
198+
path=invocation_path / "src/pkg",
199+
parts=[],
179200
)
180201

181202
with pytest.raises(
@@ -186,10 +207,12 @@ def test_pypath(self, invocation_path: Path) -> None:
186207
)
187208

188209
def test_parametrized_name_with_colons(self, invocation_path: Path) -> None:
189-
ret = resolve_collection_argument(
210+
assert resolve_collection_argument(
190211
invocation_path, "src/pkg/test.py::test[a::b]"
212+
) == CollectionArgument(
213+
path=invocation_path / "src/pkg/test.py",
214+
parts=["test[a::b]"],
191215
)
192-
assert ret == (invocation_path / "src/pkg/test.py", ["test[a::b]"])
193216

194217
def test_does_not_exist(self, invocation_path: Path) -> None:
195218
"""Given a file/module that does not exist raises UsageError."""
@@ -209,17 +232,22 @@ def test_does_not_exist(self, invocation_path: Path) -> None:
209232
def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> None:
210233
"""Absolute paths resolve back to absolute paths."""
211234
full_path = str(invocation_path / "src")
212-
assert resolve_collection_argument(invocation_path, full_path) == (
213-
Path(os.path.abspath("src")),
214-
[],
235+
assert resolve_collection_argument(
236+
invocation_path, full_path
237+
) == CollectionArgument(
238+
path=Path(os.path.abspath("src")),
239+
parts=[],
215240
)
216241

217242
# ensure full paths given in the command-line without the drive letter resolve
218243
# to the full path correctly (#7628)
219244
drive, full_path_without_drive = os.path.splitdrive(full_path)
220245
assert resolve_collection_argument(
221246
invocation_path, full_path_without_drive
222-
) == (Path(os.path.abspath("src")), [])
247+
) == CollectionArgument(
248+
path=Path(os.path.abspath("src")),
249+
parts=[],
250+
)
223251

224252

225253
def test_module_full_path_without_drive(pytester: Pytester) -> None:

0 commit comments

Comments
 (0)