main: model the result of `resolve_collection_arguments` as a dataclass

In preparation of adding more info to it.
This commit is contained in:
Ran Benita 2024-03-01 11:47:30 +02:00
parent ff4c3b2873
commit 5e0d11746c
2 changed files with 86 additions and 36 deletions

View File

@ -564,7 +564,7 @@ class Session(nodes.Collector):
self._initialpaths: FrozenSet[Path] = frozenset() self._initialpaths: FrozenSet[Path] = frozenset()
self._initialpaths_with_parents: FrozenSet[Path] = frozenset() self._initialpaths_with_parents: FrozenSet[Path] = frozenset()
self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = [] self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []
self._initial_parts: List[Tuple[Path, List[str]]] = [] self._initial_parts: List[CollectionArgument] = []
self._collection_cache: Dict[nodes.Collector, CollectReport] = {} self._collection_cache: Dict[nodes.Collector, CollectReport] = {}
self.items: List[nodes.Item] = [] self.items: List[nodes.Item] = []
@ -770,15 +770,15 @@ class Session(nodes.Collector):
initialpaths: List[Path] = [] initialpaths: List[Path] = []
initialpaths_with_parents: List[Path] = [] initialpaths_with_parents: List[Path] = []
for arg in args: for arg in args:
fspath, parts = resolve_collection_argument( collection_argument = resolve_collection_argument(
self.config.invocation_params.dir, self.config.invocation_params.dir,
arg, arg,
as_pypath=self.config.option.pyargs, as_pypath=self.config.option.pyargs,
) )
self._initial_parts.append((fspath, parts)) self._initial_parts.append(collection_argument)
initialpaths.append(fspath) initialpaths.append(collection_argument.path)
initialpaths_with_parents.append(fspath) initialpaths_with_parents.append(collection_argument.path)
initialpaths_with_parents.extend(fspath.parents) initialpaths_with_parents.extend(collection_argument.path.parents)
self._initialpaths = frozenset(initialpaths) self._initialpaths = frozenset(initialpaths)
self._initialpaths_with_parents = frozenset(initialpaths_with_parents) self._initialpaths_with_parents = frozenset(initialpaths_with_parents)
@ -840,10 +840,13 @@ class Session(nodes.Collector):
pm = self.config.pluginmanager pm = self.config.pluginmanager
for argpath, names in self._initial_parts: for collection_argument in self._initial_parts:
self.trace("processing argument", (argpath, names)) self.trace("processing argument", collection_argument)
self.trace.root.indent += 1 self.trace.root.indent += 1
argpath = collection_argument.path
names = collection_argument.parts
# resolve_collection_argument() ensures this. # resolve_collection_argument() ensures this.
if argpath.is_dir(): if argpath.is_dir():
assert not names, f"invalid arg {(argpath, names)!r}" assert not names, f"invalid arg {(argpath, names)!r}"
@ -862,7 +865,7 @@ class Session(nodes.Collector):
notfound_collectors = [] notfound_collectors = []
work: List[ work: List[
Tuple[Union[nodes.Collector, nodes.Item], List[Union[Path, str]]] Tuple[Union[nodes.Collector, nodes.Item], List[Union[Path, str]]]
] = [(self, paths + names)] ] = [(self, [*paths, *names])]
while work: while work:
matchnode, matchparts = work.pop() matchnode, matchparts = work.pop()
@ -971,9 +974,17 @@ def search_pypath(module_name: str) -> str:
return spec.origin return spec.origin
@dataclasses.dataclass(frozen=True)
class CollectionArgument:
"""A resolved collection argument."""
path: Path
parts: Sequence[str]
def resolve_collection_argument( def resolve_collection_argument(
invocation_path: Path, arg: str, *, as_pypath: bool = False invocation_path: Path, arg: str, *, as_pypath: bool = False
) -> Tuple[Path, List[str]]: ) -> CollectionArgument:
"""Parse path arguments optionally containing selection parts and return (fspath, names). """Parse path arguments optionally containing selection parts and return (fspath, names).
Command-line arguments can point to files and/or directories, and optionally contain Command-line arguments can point to files and/or directories, and optionally contain
@ -981,9 +992,12 @@ def resolve_collection_argument(
"pkg/tests/test_foo.py::TestClass::test_foo" "pkg/tests/test_foo.py::TestClass::test_foo"
This function ensures the path exists, and returns a tuple: This function ensures the path exists, and returns a resolved `CollectionArgument`:
(Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"]) CollectionArgument(
path=Path("/full/path/to/pkg/tests/test_foo.py"),
parts=["TestClass", "test_foo"],
)
When as_pypath is True, expects that the command-line argument actually contains When as_pypath is True, expects that the command-line argument actually contains
module paths instead of file-system paths: module paths instead of file-system paths:
@ -991,7 +1005,12 @@ def resolve_collection_argument(
"pkg.tests.test_foo::TestClass::test_foo" "pkg.tests.test_foo::TestClass::test_foo"
In which case we search sys.path for a matching module, and then return the *path* to the In which case we search sys.path for a matching module, and then return the *path* to the
found module. found module, which may look like this:
CollectionArgument(
path=Path("/home/u/myvenv/lib/site-packages/pkg/tests/test_foo.py"),
parts=["TestClass", "test_foo"],
)
If the path doesn't exist, raise UsageError. If the path doesn't exist, raise UsageError.
If the path is a directory and selection parts are present, raise UsageError. If the path is a directory and selection parts are present, raise UsageError.
@ -1018,4 +1037,7 @@ def resolve_collection_argument(
else "directory argument cannot contain :: selection parts: {arg}" else "directory argument cannot contain :: selection parts: {arg}"
) )
raise UsageError(msg.format(arg=arg)) raise UsageError(msg.format(arg=arg))
return fspath, parts return CollectionArgument(
path=fspath,
parts=parts,
)

View File

@ -8,6 +8,7 @@ from typing import Optional
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import UsageError from _pytest.config import UsageError
from _pytest.main import CollectionArgument
from _pytest.main import resolve_collection_argument from _pytest.main import resolve_collection_argument
from _pytest.main import validate_basetemp from _pytest.main import validate_basetemp
from _pytest.pytester import Pytester from _pytest.pytester import Pytester
@ -133,26 +134,38 @@ class TestResolveCollectionArgument:
def test_file(self, invocation_path: Path) -> None: def test_file(self, invocation_path: Path) -> None:
"""File and parts.""" """File and parts."""
assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == ( assert resolve_collection_argument(
invocation_path / "src/pkg/test.py", invocation_path, "src/pkg/test.py"
[], ) == CollectionArgument(
path=invocation_path / "src/pkg/test.py",
parts=[],
) )
assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == ( assert resolve_collection_argument(
invocation_path / "src/pkg/test.py", invocation_path, "src/pkg/test.py::"
[""], ) == CollectionArgument(
path=invocation_path / "src/pkg/test.py",
parts=[""],
) )
assert resolve_collection_argument( assert resolve_collection_argument(
invocation_path, "src/pkg/test.py::foo::bar" invocation_path, "src/pkg/test.py::foo::bar"
) == (invocation_path / "src/pkg/test.py", ["foo", "bar"]) ) == CollectionArgument(
path=invocation_path / "src/pkg/test.py",
parts=["foo", "bar"],
)
assert resolve_collection_argument( assert resolve_collection_argument(
invocation_path, "src/pkg/test.py::foo::bar::" invocation_path, "src/pkg/test.py::foo::bar::"
) == (invocation_path / "src/pkg/test.py", ["foo", "bar", ""]) ) == CollectionArgument(
path=invocation_path / "src/pkg/test.py",
parts=["foo", "bar", ""],
)
def test_dir(self, invocation_path: Path) -> None: def test_dir(self, invocation_path: Path) -> None:
"""Directory and parts.""" """Directory and parts."""
assert resolve_collection_argument(invocation_path, "src/pkg") == ( assert resolve_collection_argument(
invocation_path / "src/pkg", invocation_path, "src/pkg"
[], ) == CollectionArgument(
path=invocation_path / "src/pkg",
parts=[],
) )
with pytest.raises( with pytest.raises(
@ -169,13 +182,21 @@ class TestResolveCollectionArgument:
"""Dotted name and parts.""" """Dotted name and parts."""
assert resolve_collection_argument( assert resolve_collection_argument(
invocation_path, "pkg.test", as_pypath=True invocation_path, "pkg.test", as_pypath=True
) == (invocation_path / "src/pkg/test.py", []) ) == CollectionArgument(
path=invocation_path / "src/pkg/test.py",
parts=[],
)
assert resolve_collection_argument( assert resolve_collection_argument(
invocation_path, "pkg.test::foo::bar", as_pypath=True invocation_path, "pkg.test::foo::bar", as_pypath=True
) == (invocation_path / "src/pkg/test.py", ["foo", "bar"]) ) == CollectionArgument(
assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == ( path=invocation_path / "src/pkg/test.py",
invocation_path / "src/pkg", parts=["foo", "bar"],
[], )
assert resolve_collection_argument(
invocation_path, "pkg", as_pypath=True
) == CollectionArgument(
path=invocation_path / "src/pkg",
parts=[],
) )
with pytest.raises( with pytest.raises(
@ -186,10 +207,12 @@ class TestResolveCollectionArgument:
) )
def test_parametrized_name_with_colons(self, invocation_path: Path) -> None: def test_parametrized_name_with_colons(self, invocation_path: Path) -> None:
ret = resolve_collection_argument( assert resolve_collection_argument(
invocation_path, "src/pkg/test.py::test[a::b]" invocation_path, "src/pkg/test.py::test[a::b]"
) == CollectionArgument(
path=invocation_path / "src/pkg/test.py",
parts=["test[a::b]"],
) )
assert ret == (invocation_path / "src/pkg/test.py", ["test[a::b]"])
def test_does_not_exist(self, invocation_path: Path) -> None: def test_does_not_exist(self, invocation_path: Path) -> None:
"""Given a file/module that does not exist raises UsageError.""" """Given a file/module that does not exist raises UsageError."""
@ -209,9 +232,11 @@ class TestResolveCollectionArgument:
def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> None: def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> None:
"""Absolute paths resolve back to absolute paths.""" """Absolute paths resolve back to absolute paths."""
full_path = str(invocation_path / "src") full_path = str(invocation_path / "src")
assert resolve_collection_argument(invocation_path, full_path) == ( assert resolve_collection_argument(
Path(os.path.abspath("src")), invocation_path, full_path
[], ) == CollectionArgument(
path=Path(os.path.abspath("src")),
parts=[],
) )
# ensure full paths given in the command-line without the drive letter resolve # ensure full paths given in the command-line without the drive letter resolve
@ -219,7 +244,10 @@ class TestResolveCollectionArgument:
drive, full_path_without_drive = os.path.splitdrive(full_path) drive, full_path_without_drive = os.path.splitdrive(full_path)
assert resolve_collection_argument( assert resolve_collection_argument(
invocation_path, full_path_without_drive invocation_path, full_path_without_drive
) == (Path(os.path.abspath("src")), []) ) == CollectionArgument(
path=Path(os.path.abspath("src")),
parts=[],
)
def test_module_full_path_without_drive(pytester: Pytester) -> None: def test_module_full_path_without_drive(pytester: Pytester) -> None: