Merge pull request #12043 from bluetech/pyargs-root
Fix collection failures due to permission errors when using `--pyargs`
This commit is contained in:
		
						commit
						6ed005161d
					
				| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8.
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ import dataclasses
 | 
				
			||||||
import fnmatch
 | 
					import fnmatch
 | 
				
			||||||
import functools
 | 
					import functools
 | 
				
			||||||
import importlib
 | 
					import importlib
 | 
				
			||||||
 | 
					import importlib.util
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
| 
						 | 
					@ -563,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] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -769,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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -839,21 +840,35 @@ 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
 | 
				
			||||||
 | 
					            module_name = collection_argument.module_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # 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}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Match the argpath from the root, e.g.
 | 
					            paths = [argpath]
 | 
				
			||||||
 | 
					            # Add relevant parents of the path, from the root, e.g.
 | 
				
			||||||
            #   /a/b/c.py -> [/, /a, /a/b, /a/b/c.py]
 | 
					            #   /a/b/c.py -> [/, /a, /a/b, /a/b/c.py]
 | 
				
			||||||
            paths = [*reversed(argpath.parents), argpath]
 | 
					            if module_name is None:
 | 
				
			||||||
            # Paths outside of the confcutdir should not be considered, unless
 | 
					                # Paths outside of the confcutdir should not be considered.
 | 
				
			||||||
            # it's the argpath itself.
 | 
					                for path in argpath.parents:
 | 
				
			||||||
            while len(paths) > 1 and not pm._is_in_confcutdir(paths[0]):
 | 
					                    if not pm._is_in_confcutdir(path):
 | 
				
			||||||
                paths = paths[1:]
 | 
					                        break
 | 
				
			||||||
 | 
					                    paths.insert(0, path)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # For --pyargs arguments, only consider paths matching the module
 | 
				
			||||||
 | 
					                # name. Paths beyond the package hierarchy are not included.
 | 
				
			||||||
 | 
					                module_name_parts = module_name.split(".")
 | 
				
			||||||
 | 
					                for i, path in enumerate(argpath.parents, 2):
 | 
				
			||||||
 | 
					                    if i > len(module_name_parts) or path.stem != module_name_parts[-i]:
 | 
				
			||||||
 | 
					                        break
 | 
				
			||||||
 | 
					                    paths.insert(0, path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Start going over the parts from the root, collecting each level
 | 
					            # Start going over the parts from the root, collecting each level
 | 
				
			||||||
            # and discarding all nodes which don't match the level's part.
 | 
					            # and discarding all nodes which don't match the level's part.
 | 
				
			||||||
| 
						 | 
					@ -861,7 +876,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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -953,26 +968,36 @@ class Session(nodes.Collector):
 | 
				
			||||||
                node.ihook.pytest_collectreport(report=rep)
 | 
					                node.ihook.pytest_collectreport(report=rep)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def search_pypath(module_name: str) -> str:
 | 
					def search_pypath(module_name: str) -> Optional[str]:
 | 
				
			||||||
    """Search sys.path for the given a dotted module name, and return its file system path."""
 | 
					    """Search sys.path for the given a dotted module name, and return its file
 | 
				
			||||||
 | 
					    system path if found."""
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        spec = importlib.util.find_spec(module_name)
 | 
					        spec = importlib.util.find_spec(module_name)
 | 
				
			||||||
    # AttributeError: looks like package module, but actually filename
 | 
					    # AttributeError: looks like package module, but actually filename
 | 
				
			||||||
    # ImportError: module does not exist
 | 
					    # ImportError: module does not exist
 | 
				
			||||||
    # ValueError: not a module name
 | 
					    # ValueError: not a module name
 | 
				
			||||||
    except (AttributeError, ImportError, ValueError):
 | 
					    except (AttributeError, ImportError, ValueError):
 | 
				
			||||||
        return module_name
 | 
					        return None
 | 
				
			||||||
    if spec is None or spec.origin is None or spec.origin == "namespace":
 | 
					    if spec is None or spec.origin is None or spec.origin == "namespace":
 | 
				
			||||||
        return module_name
 | 
					        return None
 | 
				
			||||||
    elif spec.submodule_search_locations:
 | 
					    elif spec.submodule_search_locations:
 | 
				
			||||||
        return os.path.dirname(spec.origin)
 | 
					        return os.path.dirname(spec.origin)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        return spec.origin
 | 
					        return spec.origin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclasses.dataclass(frozen=True)
 | 
				
			||||||
 | 
					class CollectionArgument:
 | 
				
			||||||
 | 
					    """A resolved collection argument."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    path: Path
 | 
				
			||||||
 | 
					    parts: Sequence[str]
 | 
				
			||||||
 | 
					    module_name: Optional[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
 | 
				
			||||||
| 
						 | 
					@ -980,9 +1005,13 @@ 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"],
 | 
				
			||||||
 | 
					            module_name=None,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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:
 | 
				
			||||||
| 
						 | 
					@ -990,7 +1019,13 @@ 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"],
 | 
				
			||||||
 | 
					            module_name="pkg.tests.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.
 | 
				
			||||||
| 
						 | 
					@ -999,8 +1034,12 @@ def resolve_collection_argument(
 | 
				
			||||||
    strpath, *parts = base.split("::")
 | 
					    strpath, *parts = base.split("::")
 | 
				
			||||||
    if parts:
 | 
					    if parts:
 | 
				
			||||||
        parts[-1] = f"{parts[-1]}{squacket}{rest}"
 | 
					        parts[-1] = f"{parts[-1]}{squacket}{rest}"
 | 
				
			||||||
 | 
					    module_name = None
 | 
				
			||||||
    if as_pypath:
 | 
					    if as_pypath:
 | 
				
			||||||
        strpath = search_pypath(strpath)
 | 
					        pyarg_strpath = search_pypath(strpath)
 | 
				
			||||||
 | 
					        if pyarg_strpath is not None:
 | 
				
			||||||
 | 
					            module_name = strpath
 | 
				
			||||||
 | 
					            strpath = pyarg_strpath
 | 
				
			||||||
    fspath = invocation_path / strpath
 | 
					    fspath = invocation_path / strpath
 | 
				
			||||||
    fspath = absolutepath(fspath)
 | 
					    fspath = absolutepath(fspath)
 | 
				
			||||||
    if not safe_exists(fspath):
 | 
					    if not safe_exists(fspath):
 | 
				
			||||||
| 
						 | 
					@ -1017,4 +1056,8 @@ 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,
 | 
				
			||||||
 | 
					        module_name=module_name,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1787,3 +1787,48 @@ def test_collect_short_file_windows(pytester: Pytester) -> None:
 | 
				
			||||||
    test_file.write_text("def test(): pass", encoding="UTF-8")
 | 
					    test_file.write_text("def test(): pass", encoding="UTF-8")
 | 
				
			||||||
    result = pytester.runpytest(short_path)
 | 
					    result = pytester.runpytest(short_path)
 | 
				
			||||||
    assert result.parseoutcomes() == {"passed": 1}
 | 
					    assert result.parseoutcomes() == {"passed": 1}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_pyargs_collection_tree(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
 | 
				
			||||||
 | 
					    """When using `--pyargs`, the collection tree of a pyargs collection
 | 
				
			||||||
 | 
					    argument should only include parents in the import path, not up to confcutdir.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Regression test for #11904.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    site_packages = pytester.path / "venv/lib/site-packages"
 | 
				
			||||||
 | 
					    site_packages.mkdir(parents=True)
 | 
				
			||||||
 | 
					    monkeypatch.syspath_prepend(site_packages)
 | 
				
			||||||
 | 
					    pytester.makepyfile(
 | 
				
			||||||
 | 
					        **{
 | 
				
			||||||
 | 
					            "venv/lib/site-packages/pkg/__init__.py": "",
 | 
				
			||||||
 | 
					            "venv/lib/site-packages/pkg/sub/__init__.py": "",
 | 
				
			||||||
 | 
					            "venv/lib/site-packages/pkg/sub/test_it.py": "def test(): pass",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result = pytester.runpytest("--pyargs", "--collect-only", "pkg.sub.test_it")
 | 
				
			||||||
 | 
					    assert result.ret == ExitCode.OK
 | 
				
			||||||
 | 
					    result.stdout.fnmatch_lines(
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "<Package venv/lib/site-packages/pkg>",
 | 
				
			||||||
 | 
					            "  <Package sub>",
 | 
				
			||||||
 | 
					            "    <Module test_it.py>",
 | 
				
			||||||
 | 
					            "      <Function test>",
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        consecutive=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Now with an unrelated rootdir with unrelated files.
 | 
				
			||||||
 | 
					    monkeypatch.chdir(tempfile.gettempdir())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result = pytester.runpytest("--pyargs", "--collect-only", "pkg.sub.test_it")
 | 
				
			||||||
 | 
					    assert result.ret == ExitCode.OK
 | 
				
			||||||
 | 
					    result.stdout.fnmatch_lines(
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "<Package *pkg>",
 | 
				
			||||||
 | 
					            "  <Package sub>",
 | 
				
			||||||
 | 
					            "    <Module test_it.py>",
 | 
				
			||||||
 | 
					            "      <Function test>",
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        consecutive=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,43 @@ 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=[],
 | 
				
			||||||
 | 
					            module_name=None,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        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=[""],
 | 
				
			||||||
 | 
					            module_name=None,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        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"],
 | 
				
			||||||
 | 
					            module_name=None,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        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", ""],
 | 
				
			||||||
 | 
					            module_name=None,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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=[],
 | 
				
			||||||
 | 
					            module_name=None,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with pytest.raises(
 | 
					        with pytest.raises(
 | 
				
			||||||
| 
						 | 
					@ -169,13 +187,24 @@ 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=[],
 | 
				
			||||||
 | 
					            module_name="pkg.test",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        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"],
 | 
				
			||||||
            [],
 | 
					            module_name="pkg.test",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        assert resolve_collection_argument(
 | 
				
			||||||
 | 
					            invocation_path, "pkg", as_pypath=True
 | 
				
			||||||
 | 
					        ) == CollectionArgument(
 | 
				
			||||||
 | 
					            path=invocation_path / "src/pkg",
 | 
				
			||||||
 | 
					            parts=[],
 | 
				
			||||||
 | 
					            module_name="pkg",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with pytest.raises(
 | 
					        with pytest.raises(
 | 
				
			||||||
| 
						 | 
					@ -186,10 +215,13 @@ 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]"],
 | 
				
			||||||
 | 
					            module_name=None,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        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 +241,12 @@ 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=[],
 | 
				
			||||||
 | 
					            module_name=None,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # 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 +254,11 @@ 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=[],
 | 
				
			||||||
 | 
					            module_name=None,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_module_full_path_without_drive(pytester: Pytester) -> None:
 | 
					def test_module_full_path_without_drive(pytester: Pytester) -> None:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue