Skip to content
Draft
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
8 changes: 8 additions & 0 deletions changelog/2720.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Plugins now contribute their own hookspecs via :hook:`pytest_addhooks` as soon as they are
registered, instead of only when a :class:`~pytest.Config` parses a command line.

Previously a :class:`~pytest.PytestPluginManager` obtained outside a full pytest run - for
example from ``_pytest.config.get_plugin_manager()``, which is documented for integration
with other tools - never called :hook:`pytest_addhooks` on the plugins registered with it.
Such a plugin's own hook implementations were then rejected with
``PluginValidationError: unknown hook ...``.
8 changes: 4 additions & 4 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,10 @@ def __init__(self) -> None:

self.add_hookspecs(_pytest.hookspec)
self.register(self)
# Historic, so that plugins registered later - which is all of them -
# get to contribute their own hookspecs as they are registered, rather
# than only if and when a Config gets around to parsing a command line.
self.hook.pytest_addhooks.call_historic(kwargs=dict(pluginmanager=self))
if os.environ.get("PYTEST_DEBUG"):
err: IO[str] = sys.stderr
encoding: str = getattr(err, "encoding", "utf8")
Expand Down Expand Up @@ -1634,10 +1638,6 @@ def parse(self, args: list[str], addopts: bool = True) -> None:
"can only parse cmdline args at most once per Config object"
)

self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager)
)

if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
if len(env_addopts):
Expand Down
25 changes: 21 additions & 4 deletions testing/test_pluginmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ def pytest_myhook(xyz):
"""
)
config = _config_for_test
pm = config.pluginmanager
pm.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=config.pluginmanager)
)
config.pluginmanager._importconftest(
conf,
importmode="prepend",
Expand All @@ -60,6 +56,27 @@ def pytest_myhook(xyz):
res = config.hook.pytest_myhook(xyz=10)
assert res == [11]

def test_addhooks_on_registration(self, pytestpm: PytestPluginManager) -> None:
"""A plugin contributes its hookspecs when it is registered, not when
some Config gets around to parsing a command line (#2720)."""

class NewHooks:
def pytest_myhook(self, xyz):
"""New hook"""

class Plugin:
def pytest_addhooks(self, pluginmanager):
pluginmanager.add_hookspecs(NewHooks)

def pytest_myhook(self, xyz):
return xyz + 1

pytestpm.register(Plugin())

assert pytestpm.hook.pytest_myhook.has_spec()
pytestpm.check_pending()
assert pytestpm.hook.pytest_myhook(xyz=10) == [11]

def test_addhooks_nohooks(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
Expand Down