Skip to content

Commit

Permalink
hookspec: add pathlib.Path alternatives to py.path.local parameters i…
Browse files Browse the repository at this point in the history
…n 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.
  • Loading branch information
bluetech committed Dec 14, 2020
1 parent 1620b3d commit d0842c5
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 25 deletions.
7 changes: 7 additions & 0 deletions changelog/8144.feature.rst
Original file line number Diff line number Diff line change
@@ -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).
42 changes: 36 additions & 6 deletions src/_pytest/hookspec.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -261,27 +262,39 @@ 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
more specific hooks.
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.
"""


Expand Down Expand Up @@ -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.
Expand All @@ -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.
"""


Expand Down Expand Up @@ -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::
Expand All @@ -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 <pluginorder>`.
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.
Expand All @@ -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.
Expand All @@ -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 <plugin-hookorder>`.
versionchanged:: 6.3.0
The ``startpath`` parameter was added as a pathlib.Path equivalent
of the ``startdir`` parameter.
"""


Expand Down
14 changes: 9 additions & 5 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -544,14 +545,17 @@ 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(
path, path.isdir(), path.exists(), path.islink()
)
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:
Expand All @@ -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(
Expand Down
25 changes: 16 additions & 9 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -676,14 +680,17 @@ 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(
path, path.isdir(), path.exists(), path.islink()
)
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:
Expand All @@ -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()
Expand Down
7 changes: 5 additions & 2 deletions src/_pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
"""
)
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit d0842c5

Please sign in to comment.