From 0cd1e4d19da7f2557aa8864c4de8693fe749e76d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 14 Dec 2020 18:16:14 +0200 Subject: [PATCH] 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..67691294fa2 --- /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..3c1515e6960 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 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 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 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 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 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")