From 2cb34a99cbf423c50b7b6592a54f80f68bb9fdc0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 14 Dec 2020 15:54:59 +0200 Subject: [PATCH 1/2] Some py.path.local -> pathlib.Path --- src/_pytest/assertion/rewrite.py | 5 ++- src/_pytest/config/argparsing.py | 15 +++++---- src/_pytest/fixtures.py | 15 ++++----- src/_pytest/main.py | 43 +++++++++++++------------ src/_pytest/monkeypatch.py | 13 ++------ src/_pytest/nodes.py | 10 ++++-- src/_pytest/pathlib.py | 6 ++-- testing/test_assertrewrite.py | 6 ++-- testing/test_main.py | 54 ++++++++++++++------------------ testing/test_nodes.py | 11 +++++-- 10 files changed, 84 insertions(+), 94 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 805d4c8b35b..a01be76b4d3 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -27,8 +27,6 @@ from typing import TYPE_CHECKING from typing import Union -import py - from _pytest._io.saferepr import saferepr from _pytest._version import version from _pytest.assertion import util @@ -37,6 +35,7 @@ ) from _pytest.config import Config from _pytest.main import Session +from _pytest.pathlib import absolutepath from _pytest.pathlib import fnmatch_ex from _pytest.store import StoreKey @@ -215,7 +214,7 @@ def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: return True if self.session is not None: - if self.session.isinitpath(py.path.local(fn)): + if self.session.isinitpath(absolutepath(fn)): state.trace(f"matched test file (was specified on cmdline): {fn!r}") return True diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 9a481965526..5a09ea781e6 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -1,4 +1,5 @@ import argparse +import os import sys import warnings from gettext import gettext @@ -14,8 +15,6 @@ from typing import TYPE_CHECKING from typing import Union -import py - import _pytest._io from _pytest.compat import final from _pytest.config.exceptions import UsageError @@ -97,14 +96,14 @@ def addoption(self, *opts: str, **attrs: Any) -> None: def parse( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: from _pytest._argcomplete import try_argcomplete self.optparser = self._getparser() try_argcomplete(self.optparser) - strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + strargs = [os.fspath(x) for x in args] return self.optparser.parse_args(strargs, namespace=namespace) def _getparser(self) -> "MyOptionParser": @@ -128,7 +127,7 @@ def _getparser(self) -> "MyOptionParser": def parse_setoption( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], option: argparse.Namespace, namespace: Optional[argparse.Namespace] = None, ) -> List[str]: @@ -139,7 +138,7 @@ def parse_setoption( def parse_known_args( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: """Parse and return a namespace object with known arguments at this point.""" @@ -147,13 +146,13 @@ def parse_known_args( def parse_known_and_unknown_args( self, - args: Sequence[Union[str, py.path.local]], + args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> Tuple[argparse.Namespace, List[str]]: """Parse and return a namespace object with known arguments, and the remaining arguments unknown at this point.""" optparser = self._getparser() - strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + strargs = [os.fspath(x) for x in args] return optparser.parse_known_args(strargs, namespace=namespace) def addini( diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 273bcafd393..c24ab7069cb 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -648,12 +648,13 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: if has_params: frame = inspect.stack()[3] frameinfo = inspect.getframeinfo(frame[0]) - source_path = py.path.local(frameinfo.filename) + source_path = absolutepath(frameinfo.filename) source_lineno = frameinfo.lineno - rel_source_path = source_path.relto(funcitem.config.rootdir) - if rel_source_path: - source_path_str = rel_source_path - else: + try: + source_path_str = str( + source_path.relative_to(funcitem.config.rootpath) + ) + except ValueError: source_path_str = str(source_path) msg = ( "The requested fixture has no parameter defined for test:\n" @@ -876,7 +877,7 @@ def formatrepr(self) -> "FixtureLookupErrorRepr": class FixtureLookupErrorRepr(TerminalRepr): def __init__( self, - filename: Union[str, py.path.local], + filename: Union[str, "os.PathLike[str]"], firstlineno: int, tblines: Sequence[str], errorstring: str, @@ -903,7 +904,7 @@ def toterminal(self, tw: TerminalWriter) -> None: f"{FormattedExcinfo.flow_marker} {line.strip()}", red=True, ) tw.line() - tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) + tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1)) def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": diff --git a/src/_pytest/main.py b/src/_pytest/main.py index eab3c9afd27..d536f9d8066 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -467,7 +467,7 @@ def __init__(self, config: Config) -> None: self.shouldfail: Union[bool, str] = False self.trace = config.trace.root.get("collection") self.startdir = config.invocation_dir - self._initialpaths: FrozenSet[py.path.local] = frozenset() + self._initialpaths: FrozenSet[Path] = frozenset() self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath) @@ -510,8 +510,8 @@ def pytest_runtest_logreport( pytest_collectreport = pytest_runtest_logreport - def isinitpath(self, path: py.path.local) -> bool: - return path in self._initialpaths + def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool: + return Path(path) in self._initialpaths def gethookproxy(self, fspath: "os.PathLike[str]"): # Check if we have the common case of running @@ -601,14 +601,14 @@ def perform_collect( self.trace.root.indent += 1 self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = [] - self._initial_parts: List[Tuple[py.path.local, List[str]]] = [] + self._initial_parts: List[Tuple[Path, List[str]]] = [] self.items: List[nodes.Item] = [] hook = self.config.hook items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items try: - initialpaths: List[py.path.local] = [] + initialpaths: List[Path] = [] for arg in args: fspath, parts = resolve_collection_argument( self.config.invocation_params.dir, @@ -669,13 +669,13 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # No point in finding packages when collecting doctests. if not self.config.getoption("doctestmodules", False): pm = self.config.pluginmanager - confcutdir = py.path.local(pm._confcutdir) if pm._confcutdir else None - for parent in reversed(argpath.parts()): - if confcutdir and confcutdir.relto(parent): + confcutdir = pm._confcutdir + for parent in (argpath, *argpath.parents): + if confcutdir and parent in confcutdir.parents: break - if parent.isdir(): - pkginit = parent.join("__init__.py") + if parent.is_dir(): + pkginit = py.path.local(parent / "__init__.py") if pkginit.isfile() and pkginit not in node_cache1: col = self._collectfile(pkginit, handle_dupes=False) if col: @@ -685,7 +685,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # If it's a directory argument, recurse and look for any Subpackages. # Let the Package collector deal with subnodes, don't collect here. - if argpath.check(dir=1): + if argpath.is_dir(): assert not names, "invalid arg {!r}".format((argpath, names)) seen_dirs: Set[py.path.local] = set() @@ -717,15 +717,16 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: node_cache2[key] = x yield x else: - assert argpath.check(file=1) + assert argpath.is_file() - if argpath in node_cache1: - col = node_cache1[argpath] + argpath_ = py.path.local(argpath) + if argpath_ in node_cache1: + col = node_cache1[argpath_] else: - collect_root = pkg_roots.get(argpath.dirname, self) - col = collect_root._collectfile(argpath, handle_dupes=False) + collect_root = pkg_roots.get(argpath_.dirname, self) + col = collect_root._collectfile(argpath_, handle_dupes=False) if col: - node_cache1[argpath] = col + node_cache1[argpath_] = col matching = [] work: List[ @@ -782,9 +783,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # first yielded item will be the __init__ Module itself, so # just use that. If this special case isn't taken, then all the # files in the package will be yielded. - if argpath.basename == "__init__.py" and isinstance( - matching[0], Package - ): + if argpath.name == "__init__.py" and isinstance(matching[0], Package): try: yield next(iter(matching[0].collect())) except StopIteration: @@ -833,7 +832,7 @@ def search_pypath(module_name: str) -> str: def resolve_collection_argument( invocation_path: Path, arg: str, *, as_pypath: bool = False -) -> Tuple[py.path.local, List[str]]: +) -> Tuple[Path, List[str]]: """Parse path arguments optionally containing selection parts and return (fspath, names). Command-line arguments can point to files and/or directories, and optionally contain @@ -875,4 +874,4 @@ def resolve_collection_argument( else "directory argument cannot contain :: selection parts: {arg}" ) raise UsageError(msg.format(arg=arg)) - return py.path.local(str(fspath)), parts + return fspath, parts diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index a052f693ac0..d012b8a535a 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -4,7 +4,6 @@ import sys import warnings from contextlib import contextmanager -from pathlib import Path from typing import Any from typing import Generator from typing import List @@ -325,20 +324,14 @@ def syspath_prepend(self, path) -> None: invalidate_caches() - def chdir(self, path) -> None: + def chdir(self, path: Union[str, "os.PathLike[str]"]) -> None: """Change the current working directory to the specified path. - Path can be a string or a py.path.local object. + Path can be a string or a path object. """ if self._cwd is None: self._cwd = os.getcwd() - if hasattr(path, "chdir"): - path.chdir() - elif isinstance(path, Path): - # Modern python uses the fspath protocol here LEGACY - os.chdir(str(path)) - else: - os.chdir(path) + os.chdir(path) def undo(self) -> None: """Undo previous changes. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 98bd581b96d..fee0770eb2b 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -480,10 +480,14 @@ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: excinfo.traceback = ntraceback.filter() -def _check_initialpaths_for_relpath(session, fspath): +def _check_initialpaths_for_relpath( + session: "Session", fspath: py.path.local +) -> Optional[str]: for initial_path in session._initialpaths: - if fspath.common(initial_path) == initial_path: - return fspath.relto(initial_path) + initial_path_ = py.path.local(initial_path) + if fspath.common(initial_path_) == initial_path_: + return fspath.relto(initial_path_) + return None class FSCollector(Collector): diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 8875a28f84b..2e452eb1cc9 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -30,8 +30,6 @@ from typing import TypeVar from typing import Union -import py - from _pytest.compat import assert_never from _pytest.outcomes import skip from _pytest.warning_types import PytestWarning @@ -456,7 +454,7 @@ class ImportPathMismatchError(ImportError): def import_path( - p: Union[str, py.path.local, Path], + p: Union[str, "os.PathLike[str]"], *, mode: Union[str, ImportMode] = ImportMode.prepend, ) -> ModuleType: @@ -482,7 +480,7 @@ def import_path( """ mode = ImportMode(mode) - path = Path(str(p)) + path = Path(p) if not path.exists(): raise ImportError(path) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 84d5276e729..ffe18260f90 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -17,8 +17,6 @@ from typing import Optional from typing import Set -import py - import _pytest._code import pytest from _pytest.assertion import util @@ -1311,7 +1309,7 @@ def hook( import importlib.machinery self.find_spec_calls: List[str] = [] - self.initial_paths: Set[py.path.local] = set() + self.initial_paths: Set[Path] = set() class StubSession: _initialpaths = self.initial_paths @@ -1346,7 +1344,7 @@ def fix(): return 1 pytester.makepyfile(test_foo="def test_foo(): pass") pytester.makepyfile(bar="def bar(): pass") foobar_path = pytester.makepyfile(foobar="def foobar(): pass") - self.initial_paths.add(py.path.local(foobar_path)) + self.initial_paths.add(foobar_path) # conftest files should always be rewritten assert hook.find_spec("conftest") is not None diff --git a/testing/test_main.py b/testing/test_main.py index 3e94668e82f..f45607abc30 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -4,13 +4,12 @@ from pathlib import Path from typing import Optional -import py.path - import pytest from _pytest.config import ExitCode from _pytest.config import UsageError from _pytest.main import resolve_collection_argument from _pytest.main import validate_basetemp +from _pytest.pytester import Pytester from _pytest.pytester import Testdir @@ -109,40 +108,37 @@ def test_validate_basetemp_integration(testdir): class TestResolveCollectionArgument: @pytest.fixture - def invocation_dir(self, testdir: Testdir) -> py.path.local: - testdir.syspathinsert(str(testdir.tmpdir / "src")) - testdir.chdir() - - pkg = testdir.tmpdir.join("src/pkg").ensure_dir() - pkg.join("__init__.py").ensure() - pkg.join("test.py").ensure() - return testdir.tmpdir + def invocation_path(self, pytester: Pytester) -> Path: + pytester.syspathinsert(pytester.path / "src") + pytester.chdir() - @pytest.fixture - def invocation_path(self, invocation_dir: py.path.local) -> Path: - return Path(str(invocation_dir)) + pkg = pytester.path.joinpath("src/pkg") + pkg.mkdir(parents=True) + pkg.joinpath("__init__.py").touch() + pkg.joinpath("test.py").touch() + return pytester.path - def test_file(self, invocation_dir: py.path.local, invocation_path: Path) -> None: + def test_file(self, invocation_path: Path) -> None: """File and parts.""" assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == ( - invocation_dir / "src/pkg/test.py", + invocation_path / "src/pkg/test.py", [], ) assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == ( - invocation_dir / "src/pkg/test.py", + invocation_path / "src/pkg/test.py", [""], ) assert resolve_collection_argument( invocation_path, "src/pkg/test.py::foo::bar" - ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"]) + ) == (invocation_path / "src/pkg/test.py", ["foo", "bar"]) assert resolve_collection_argument( invocation_path, "src/pkg/test.py::foo::bar::" - ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar", ""]) + ) == (invocation_path / "src/pkg/test.py", ["foo", "bar", ""]) - def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None: + def test_dir(self, invocation_path: Path) -> None: """Directory and parts.""" assert resolve_collection_argument(invocation_path, "src/pkg") == ( - invocation_dir / "src/pkg", + invocation_path / "src/pkg", [], ) @@ -156,16 +152,16 @@ def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None ): resolve_collection_argument(invocation_path, "src/pkg::foo::bar") - def test_pypath(self, invocation_dir: py.path.local, invocation_path: Path) -> None: + def test_pypath(self, invocation_path: Path) -> None: """Dotted name and parts.""" assert resolve_collection_argument( invocation_path, "pkg.test", as_pypath=True - ) == (invocation_dir / "src/pkg/test.py", []) + ) == (invocation_path / "src/pkg/test.py", []) assert resolve_collection_argument( invocation_path, "pkg.test::foo::bar", as_pypath=True - ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"]) + ) == (invocation_path / "src/pkg/test.py", ["foo", "bar"]) assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == ( - invocation_dir / "src/pkg", + invocation_path / "src/pkg", [], ) @@ -191,13 +187,11 @@ def test_does_not_exist(self, invocation_path: Path) -> None: ): resolve_collection_argument(invocation_path, "foobar", as_pypath=True) - def test_absolute_paths_are_resolved_correctly( - self, invocation_dir: py.path.local, invocation_path: Path - ) -> None: + def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> None: """Absolute paths resolve back to absolute paths.""" - full_path = str(invocation_dir / "src") + full_path = str(invocation_path / "src") assert resolve_collection_argument(invocation_path, full_path) == ( - py.path.local(os.path.abspath("src")), + Path(os.path.abspath("src")), [], ) @@ -206,7 +200,7 @@ def test_absolute_paths_are_resolved_correctly( drive, full_path_without_drive = os.path.splitdrive(full_path) assert resolve_collection_argument( invocation_path, full_path_without_drive - ) == (py.path.local(os.path.abspath("src")), []) + ) == (Path(os.path.abspath("src")), []) def test_module_full_path_without_drive(testdir): diff --git a/testing/test_nodes.py b/testing/test_nodes.py index f3824c57090..bae31f0a39c 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -1,3 +1,4 @@ +from typing import cast from typing import List from typing import Type @@ -73,17 +74,21 @@ def test__check_initialpaths_for_relpath() -> None: class FakeSession1: _initialpaths = [cwd] - assert nodes._check_initialpaths_for_relpath(FakeSession1, cwd) == "" + session = cast(pytest.Session, FakeSession1) + + assert nodes._check_initialpaths_for_relpath(session, cwd) == "" sub = cwd.join("file") class FakeSession2: _initialpaths = [cwd] - assert nodes._check_initialpaths_for_relpath(FakeSession2, sub) == "file" + session = cast(pytest.Session, FakeSession2) + + assert nodes._check_initialpaths_for_relpath(session, sub) == "file" outside = py.path.local("/outside") - assert nodes._check_initialpaths_for_relpath(FakeSession2, outside) is None + assert nodes._check_initialpaths_for_relpath(session, outside) is None def test_failure_with_changed_cwd(pytester: Pytester) -> None: From 592b32bd69cb43aace8cd5525fa0b3712ee767be Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 14 Dec 2020 18:16:14 +0200 Subject: [PATCH 2/2] hookspec: add pathlib.Path alternatives to py.path.local parameters in hooks As part of the ongoing migration for py.path to pathlib, make sure all hooks which take a py.path.local also take an equivalent pathlib.Path. --- changelog/8144.feature.rst | 7 +++++++ src/_pytest/hookspec.py | 42 ++++++++++++++++++++++++++++++++------ src/_pytest/main.py | 14 ++++++++----- src/_pytest/python.py | 25 +++++++++++++++-------- src/_pytest/terminal.py | 7 +++++-- testing/test_terminal.py | 6 +++--- 6 files changed, 76 insertions(+), 25 deletions(-) create mode 100644 changelog/8144.feature.rst diff --git a/changelog/8144.feature.rst b/changelog/8144.feature.rst new file mode 100644 index 00000000000..01f40e21521 --- /dev/null +++ b/changelog/8144.feature.rst @@ -0,0 +1,7 @@ +The following hooks now receive an additional ``pathlib.Path`` argument, equivalent to an existing ``py.path.local`` argument: + +- :func:`pytest_ignore_collect <_pytest.hookspec.pytest_ignore_collect>` - The ``fspath`` parameter (equivalent to existing ``path`` parameter). +- :func:`pytest_collect_file <_pytest.hookspec.pytest_collect_file>` - The ``fspath`` parameter (equivalent to existing ``path`` parameter). +- :func:`pytest_pycollect_makemodule <_pytest.hookspec.pytest_pycollect_makemodule>` - The ``fspath`` parameter (equivalent to existing ``path`` parameter). +- :func:`pytest_report_header <_pytest.hookspec.pytest_report_header>` - The ``startpath`` parameter (equivalent to existing ``startdir`` parameter). +- :func:`pytest_report_collectionfinish <_pytest.hookspec.pytest_report_collectionfinish>` - The ``startpath`` parameter (equivalent to existing ``startdir`` parameter). diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index e499b742c7e..22bebf5b783 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,5 +1,6 @@ """Hook specifications for pytest plugins which are invoked by pytest itself and by builtin plugins.""" +from pathlib import Path from typing import Any from typing import Dict from typing import List @@ -261,7 +262,9 @@ def pytest_collection_finish(session: "Session") -> None: @hookspec(firstresult=True) -def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[bool]: +def pytest_ignore_collect( + fspath: Path, path: py.path.local, config: "Config" +) -> Optional[bool]: """Return True to prevent considering this path for collection. This hook is consulted for all files and directories prior to calling @@ -269,19 +272,29 @@ def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[boo Stops at first non-None result, see :ref:`firstresult`. + :param pathlib.Path fspath: The path to analyze. :param py.path.local path: The path to analyze. :param _pytest.config.Config config: The pytest config object. + + .. versionchanged:: 6.3.0 + The ``fspath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. """ def pytest_collect_file( - path: py.path.local, parent: "Collector" + fspath: Path, path: py.path.local, parent: "Collector" ) -> "Optional[Collector]": """Create a Collector for the given path, or None if not relevant. The new node needs to have the specified ``parent`` as a parent. + :param pathlib.Path fspath: The path to analyze. :param py.path.local path: The path to collect. + + .. versionchanged:: 6.3.0 + The ``fspath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. """ @@ -321,7 +334,9 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor @hookspec(firstresult=True) -def pytest_pycollect_makemodule(path: py.path.local, parent) -> Optional["Module"]: +def pytest_pycollect_makemodule( + fspath: Path, path: py.path.local, parent +) -> Optional["Module"]: """Return a Module collector or None for the given path. This hook will be called for each matching test module path. @@ -330,7 +345,12 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> Optional["Module Stops at first non-None result, see :ref:`firstresult`. - :param py.path.local path: The path of module to collect. + :param pathlib.Path fspath: The path of the module to collect. + :param py.path.local path: The path of the module to collect. + + .. versionchanged:: 6.3.0 + The ``fspath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``path`` parameter. """ @@ -653,11 +673,12 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No def pytest_report_header( - config: "Config", startdir: py.path.local + config: "Config", startpath: Path, startdir: py.path.local ) -> Union[str, List[str]]: """Return a string or list of strings to be displayed as header info for terminal reporting. :param _pytest.config.Config config: The pytest config object. + :param Path startpath: The starting dir. :param py.path.local startdir: The starting dir. .. note:: @@ -672,11 +693,15 @@ def pytest_report_header( This function should be implemented only in plugins or ``conftest.py`` files situated at the tests root directory due to how pytest :ref:`discovers plugins during startup `. + + .. versionchanged:: 6.3.0 + The ``startpath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``startdir`` parameter. """ def pytest_report_collectionfinish( - config: "Config", startdir: py.path.local, items: Sequence["Item"], + config: "Config", startpath: Path, startdir: py.path.local, items: Sequence["Item"], ) -> Union[str, List[str]]: """Return a string or list of strings to be displayed after collection has finished successfully. @@ -686,6 +711,7 @@ def pytest_report_collectionfinish( .. versionadded:: 3.2 :param _pytest.config.Config config: The pytest config object. + :param Path startpath: The starting path. :param py.path.local startdir: The starting dir. :param items: List of pytest items that are going to be executed; this list should not be modified. @@ -695,6 +721,10 @@ def pytest_report_collectionfinish( ran before it. If you want to have your line(s) displayed first, use :ref:`trylast=True `. + + .. versionchanged:: 6.3.0 + The ``startpath`` parameter was added as a :class:`pathlib.Path` + equivalent of the ``startdir`` parameter. """ diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d536f9d8066..e7c31ecc1d5 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -532,9 +532,10 @@ def gethookproxy(self, fspath: "os.PathLike[str]"): def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if direntry.name == "__pycache__": return False - path = py.path.local(direntry.path) - ihook = self.gethookproxy(path.dirpath()) - if ihook.pytest_ignore_collect(path=path, config=self.config): + fspath = Path(direntry.path) + path = py.path.local(fspath) + ihook = self.gethookproxy(fspath.parent) + if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") if any(path.check(fnmatch=pat) for pat in norecursepatterns): @@ -544,6 +545,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: def _collectfile( self, path: py.path.local, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: + fspath = Path(path) assert ( path.isfile() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( @@ -551,7 +553,9 @@ def _collectfile( ) ihook = self.gethookproxy(path) if not self.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): + if ihook.pytest_ignore_collect( + fspath=fspath, path=path, config=self.config + ): return () if handle_dupes: @@ -563,7 +567,7 @@ def _collectfile( else: duplicate_paths.add(path) - return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] + return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return] @overload def perform_collect( diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 407f924a5f1..18e449b9361 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -10,6 +10,7 @@ from collections import Counter from collections import defaultdict from functools import partial +from pathlib import Path from typing import Any from typing import Callable from typing import Dict @@ -187,17 +188,19 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: def pytest_collect_file( - path: py.path.local, parent: nodes.Collector + fspath: Path, path: py.path.local, parent: nodes.Collector ) -> Optional["Module"]: ext = path.ext if ext == ".py": - if not parent.session.isinitpath(path): + if not parent.session.isinitpath(fspath): if not path_matches_patterns( path, parent.config.getini("python_files") + ["__init__.py"] ): return None - ihook = parent.session.gethookproxy(path) - module: Module = ihook.pytest_pycollect_makemodule(path=path, parent=parent) + ihook = parent.session.gethookproxy(fspath) + module: Module = ihook.pytest_pycollect_makemodule( + fspath=fspath, path=path, parent=parent + ) return module return None @@ -664,9 +667,10 @@ def isinitpath(self, path: py.path.local) -> bool: def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if direntry.name == "__pycache__": return False - path = py.path.local(direntry.path) - ihook = self.session.gethookproxy(path.dirpath()) - if ihook.pytest_ignore_collect(path=path, config=self.config): + fspath = Path(direntry.path) + path = py.path.local(fspath) + ihook = self.session.gethookproxy(fspath.parent) + if ihook.pytest_ignore_collect(fspath=fspath, path=path, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") if any(path.check(fnmatch=pat) for pat in norecursepatterns): @@ -676,6 +680,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: def _collectfile( self, path: py.path.local, handle_dupes: bool = True ) -> Sequence[nodes.Collector]: + fspath = Path(path) assert ( path.isfile() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( @@ -683,7 +688,9 @@ def _collectfile( ) ihook = self.session.gethookproxy(path) if not self.session.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): + if ihook.pytest_ignore_collect( + fspath=fspath, path=path, config=self.config + ): return () if handle_dupes: @@ -695,7 +702,7 @@ def _collectfile( else: duplicate_paths.add(path) - return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] + return ihook.pytest_collect_file(fspath=fspath, path=path, parent=self) # type: ignore[no-any-return] def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: this_path = self.fspath.dirpath() diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 0e0ed70e5be..39adfaaa310 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -710,7 +710,7 @@ def pytest_sessionstart(self, session: "Session") -> None: msg += " -- " + str(sys.executable) self.write_line(msg) lines = self.config.hook.pytest_report_header( - config=self.config, startdir=self.startdir + config=self.config, startpath=self.startpath, startdir=self.startdir ) self._write_report_lines_from_hooks(lines) @@ -745,7 +745,10 @@ def pytest_collection_finish(self, session: "Session") -> None: self.report_collect(True) lines = self.config.hook.pytest_report_collectionfinish( - config=self.config, startdir=self.startdir, items=session.items + config=self.config, + startpath=self.startpath, + startdir=self.startdir, + items=session.items, ) self._write_report_lines_from_hooks(lines) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 7ad5849d4b9..6319188a75e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1010,7 +1010,7 @@ def test_more_quiet_reporting(self, pytester: Pytester) -> None: def test_report_collectionfinish_hook(self, pytester: Pytester, params) -> None: pytester.makeconftest( """ - def pytest_report_collectionfinish(config, startdir, items): + def pytest_report_collectionfinish(config, startpath, startdir, items): return ['hello from hook: {0} items'.format(len(items))] """ ) @@ -1436,8 +1436,8 @@ def pytest_report_header(config): ) pytester.mkdir("a").joinpath("conftest.py").write_text( """ -def pytest_report_header(config, startdir): - return ["line1", str(startdir)] +def pytest_report_header(config, startdir, startpath): + return ["line1", str(startpath)] """ ) result = pytester.runpytest("a")