Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/11353.trivial.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pluggy>=1.3.0 is now required. This adds typing to :class:`~pytest.PytestPluginManager`.
4 changes: 2 additions & 2 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -980,10 +980,10 @@ TestShortLogReport
.. autoclass:: pytest.TestShortLogReport()
:members:

_Result
Result
~~~~~~~

Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information.
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information.

Stash
~~~~~
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ py_modules = py
install_requires =
iniconfig
packaging
pluggy>=1.2.0,<2.0
pluggy>=1.3.0,<2.0
colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11"
tomli>=1.0.0;python_version<"3.11"
Expand Down
21 changes: 13 additions & 8 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
from typing import Union

from pluggy import HookimplMarker
from pluggy import HookimplOpts
from pluggy import HookspecMarker
from pluggy import HookspecOpts
from pluggy import PluginManager

import _pytest._code
Expand Down Expand Up @@ -440,15 +442,17 @@ def __init__(self) -> None:
# Used to know when we are importing conftests after the pytest_configure stage.
self._configured = False

def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
def parse_hookimpl_opts(
self, plugin: _PluggyPlugin, name: str
) -> Optional[HookimplOpts]:
# pytest hooks are always prefixed with "pytest_",
# so we avoid accessing possibly non-readable attributes
# (see issue #1073).
if not name.startswith("pytest_"):
return
return None
# Ignore names which can not be hooks.
if name == "pytest_plugins":
return
return None

opts = super().parse_hookimpl_opts(plugin, name)
if opts is not None:
Expand All @@ -457,18 +461,18 @@ def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
method = getattr(plugin, name)
# Consider only actual functions for hooks (#3775).
if not inspect.isroutine(method):
return
return None
# Collect unmarked hooks as long as they have the `pytest_' prefix.
return _get_legacy_hook_marks(
return _get_legacy_hook_marks( # type: ignore[return-value]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be improved, but the legacy hook stuff is going to be removed in pytest 8.1 so I didn't bother.

method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
)

def parse_hookspec_opts(self, module_or_class, name: str):
def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]:
opts = super().parse_hookspec_opts(module_or_class, name)
if opts is None:
method = getattr(module_or_class, name)
if name.startswith("pytest_"):
opts = _get_legacy_hook_marks(
opts = _get_legacy_hook_marks( # type: ignore[assignment]
method,
"spec",
("firstresult", "historic"),
Expand Down Expand Up @@ -1067,9 +1071,10 @@ def _ensure_unconfigure(self) -> None:
fin()

def get_terminal_writer(self) -> TerminalWriter:
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
"terminalreporter"
)
assert terminalreporter is not None
return terminalreporter._tw

def pytest_cmdline_parse(
Expand Down
6 changes: 5 additions & 1 deletion src/_pytest/helpconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from _pytest.config import ExitCode
from _pytest.config import PrintHelp
from _pytest.config.argparsing import Parser
from _pytest.terminal import TerminalReporter


class HelpAction(Action):
Expand Down Expand Up @@ -161,7 +162,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def showhelp(config: Config) -> None:
import textwrap

reporter = config.pluginmanager.get_plugin("terminalreporter")
reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
"terminalreporter"
)
assert reporter is not None
tw = reporter._tw
tw.write(config._parser.optparser.format_help())
tw.line()
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,8 @@ def __init__(self, config: Config) -> None:
)
if self._log_cli_enabled():
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
# Guaranteed by `_log_cli_enabled()`.
assert terminal_reporter is not None
capture_manager = config.pluginmanager.get_plugin("capturemanager")
# if capturemanager plugin is disabled, live logging still works.
self.log_cli_handler: Union[
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ def preserve_module(name):

def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
"""Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True)
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined]
self._request.addfinalizer(reprec.finish_recording)
return reprec

Expand Down
10 changes: 8 additions & 2 deletions testing/test_pluginmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,12 @@ def test_consider_module(
mod = types.ModuleType("temp")
mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
pytestpm.consider_module(mod)
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
p1 = pytestpm.get_plugin("pytest_p1")
assert p1 is not None
assert p1.__name__ == "pytest_p1"
p2 = pytestpm.get_plugin("pytest_p2")
assert p2 is not None
assert p2.__name__ == "pytest_p2"

def test_consider_module_import_module(
self, pytester: Pytester, _config_for_test: Config
Expand Down Expand Up @@ -336,6 +340,7 @@ def test_import_plugin_importname(
len2 = len(pytestpm.get_plugins())
assert len1 == len2
plugin1 = pytestpm.get_plugin("pytest_hello")
assert plugin1 is not None
assert plugin1.__name__.endswith("pytest_hello")
plugin2 = pytestpm.get_plugin("pytest_hello")
assert plugin2 is plugin1
Expand All @@ -351,6 +356,7 @@ def test_import_plugin_dotted_name(
pluginname = "pkg.plug"
pytestpm.import_plugin(pluginname)
mod = pytestpm.get_plugin("pkg.plug")
assert mod is not None
assert mod.x == 3

def test_consider_conftest_deps(
Expand Down