diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index e695f89bb..d2a9434d6 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -93,26 +93,40 @@ def iterparentnodeids(nodeid: str) -> Iterator[str]: yield nodeid +def _check_path(path: Path, fspath: LEGACY_PATH) -> None: + if Path(fspath) != path: + raise ValueError( + f"Path({fspath!r}) != {path!r}\n" + "if both path and fspath are given they need to be equal" + ) + + def _imply_path( path: Optional[Path], fspath: Optional[LEGACY_PATH] ) -> Tuple[Path, LEGACY_PATH]: if path is not None: if fspath is not None: - if Path(fspath) != path: - raise ValueError( - f"Path({fspath!r}) != {path!r}\n" - "if both path and fspath are given they need to be equal" - ) - assert Path(fspath) == path, f"{fspath} != {path}" + _check_path(path, fspath) else: fspath = legacy_path(path) return path, fspath - else: assert fspath is not None return Path(fspath), fspath +# Optimization: use _imply_path_only over _imply_path when only need Path. +# This is to avoid `legacy_path(path)` which is surprisingly heavy. +def _imply_path_only(path: Optional[Path], fspath: Optional[LEGACY_PATH]) -> Path: + if path is not None: + if fspath is not None: + _check_path(path, fspath) + return path + else: + assert fspath is not None + return Path(fspath) + + _NodeType = TypeVar("_NodeType", bound="Node") @@ -196,7 +210,9 @@ class Node(metaclass=NodeMeta): self.session = parent.session #: Filesystem path where this node was collected from (can be None). - self.path = _imply_path(path or getattr(parent, "path", None), fspath=fspath)[0] + self.path = _imply_path_only( + path or getattr(parent, "path", None), fspath=fspath + ) # The explicit annotation is to avoid publicly exposing NodeKeywords. #: Keywords/markers collected from all scopes. @@ -573,7 +589,7 @@ class FSCollector(Collector): assert path is None path = path_or_parent - path, fspath = _imply_path(path, fspath=fspath) + path = _imply_path_only(path, fspath=fspath) if name is None: name = path.name if parent is not None and parent.path != path: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 0f45d066f..1d6931f2b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -639,7 +639,6 @@ class Package(Module): ) -> None: # NOTE: Could be just the following, but kept as-is for compat. # nodes.FSCollector.__init__(self, fspath, parent=parent) - path, fspath = nodes._imply_path(path, fspath=fspath) session = parent.session nodes.FSCollector.__init__( self, @@ -650,7 +649,7 @@ class Package(Module): session=session, nodeid=nodeid, ) - self.name = path.parent.name + self.name = self.path.parent.name def setup(self) -> None: # Not using fixtures to call setup_module here because autouse fixtures diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 0a5adac56..bf796a339 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,6 +1,7 @@ import re import sys import warnings +from pathlib import Path from unittest import mock import pytest @@ -179,6 +180,13 @@ def test_hookproxy_warnings_for_fspath(tmp_path, hooktype, request): hooks.pytest_ignore_collect(config=request.config, fspath=tmp_path) + # Passing entirely *different* paths is an outright error. + with pytest.raises(ValueError, match=r"path.*fspath.*need to be equal"): + with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r: + hooks.pytest_ignore_collect( + config=request.config, path=path, fspath=Path("/bla/bla") + ) + def test_warns_none_is_deprecated(): with pytest.warns(