Skip to content

Commit 4c67d0b

Browse files
warn on ambiguous -p plugin module usage
1 parent ced0a8d commit 4c67d0b

File tree

3 files changed

+88
-1
lines changed

3 files changed

+88
-1
lines changed

changelog/14135.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pytest now warns when ``-p`` loads a module with no pytest hooks but a ``pytest11`` entry-point exists in one of its submodules, helping catch wrong plugin names when plugin autoloading is disabled.

src/_pytest/config/__init__.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,8 +878,9 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No
878878
importspec = "_pytest." + modname if modname in builtin_plugins else modname
879879
self.rewrite_hook.mark_rewrite(importspec)
880880

881+
loaded = False
881882
if consider_entry_points:
882-
loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
883+
loaded = bool(self.load_setuptools_entrypoints("pytest11", name=modname))
883884
if loaded:
884885
return
885886

@@ -900,6 +901,45 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No
900901
self.skipped_plugins.append((modname, e.msg or ""))
901902
else:
902903
self.register(mod, modname)
904+
if consider_entry_points and not loaded:
905+
self._warn_about_submodule_entrypoint_plugin(modname, mod)
906+
907+
def _warn_about_submodule_entrypoint_plugin(
908+
self, modname: str, mod: _PluggyPlugin
909+
) -> None:
910+
if self._plugin_has_pytest_hooks(mod):
911+
return
912+
913+
modname_prefix = f"{modname}."
914+
suggested = {
915+
ep.name
916+
for dist in importlib.metadata.distributions()
917+
for ep in dist.entry_points
918+
if ep.group == "pytest11"
919+
and ep.name != modname
920+
and isinstance(getattr(ep, "value", None), str)
921+
and getattr(ep, "value").split(":", 1)[0].startswith(modname_prefix)
922+
}
923+
if not suggested:
924+
return
925+
926+
suggestion = (
927+
f"-p {sorted(suggested)[0]}"
928+
if len(suggested) == 1
929+
else "one of: " + ", ".join(f"-p {name}" for name in sorted(suggested))
930+
)
931+
warnings.warn(
932+
PytestConfigWarning(
933+
f'Plugin "{modname}" contains no pytest hooks. '
934+
f"Did you mean to use {suggestion}?"
935+
),
936+
stacklevel=3,
937+
)
938+
939+
def _plugin_has_pytest_hooks(self, plugin: _PluggyPlugin) -> bool:
940+
return any(
941+
self.parse_hookimpl_opts(plugin, attr) is not None for attr in dir(plugin)
942+
)
903943

904944

905945
def _get_plugin_specs_as_list(

testing/test_config.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import re
1111
import sys
1212
import textwrap
13+
import types
1314
from typing import Any
1415

1516
import _pytest._code
@@ -30,6 +31,7 @@
3031
from _pytest.monkeypatch import MonkeyPatch
3132
from _pytest.pathlib import absolutepath
3233
from _pytest.pytester import Pytester
34+
from _pytest.warning_types import PytestConfigWarning
3335
from _pytest.warning_types import PytestDeprecationWarning
3436
import pytest
3537

@@ -1690,6 +1692,50 @@ def distributions():
16901692
# __spec__ is present when testing locally on pypy, but not in CI ????
16911693

16921694

1695+
def test_disable_plugin_autoload_warns_for_submodule_entrypoint(
1696+
pytester: Pytester, monkeypatch: MonkeyPatch
1697+
) -> None:
1698+
class DummyEntryPoint:
1699+
project_name = "pytest-recording"
1700+
name = "recording"
1701+
group = "pytest11"
1702+
version = "1.0"
1703+
value = "pytest_recording.plugin"
1704+
1705+
def load(self):
1706+
return sys.modules[self.value]
1707+
1708+
class Distribution:
1709+
metadata = {"name": "pytest-recording"}
1710+
entry_points = (DummyEntryPoint(),)
1711+
files = ()
1712+
1713+
def distributions():
1714+
return (Distribution(),)
1715+
1716+
top_level_plugin = types.ModuleType("pytest_recording")
1717+
submodule_plugin = types.ModuleType("pytest_recording.plugin")
1718+
1719+
def pytest_addoption(parser):
1720+
parser.addoption("--block-network")
1721+
1722+
setattr(submodule_plugin, "pytest_addoption", pytest_addoption)
1723+
1724+
monkeypatch.setattr(importlib.metadata, "distributions", distributions)
1725+
monkeypatch.setitem(sys.modules, "pytest_recording", top_level_plugin)
1726+
monkeypatch.setitem(sys.modules, "pytest_recording.plugin", submodule_plugin)
1727+
1728+
with pytest.warns(
1729+
PytestConfigWarning,
1730+
match=r'Plugin "pytest_recording" contains no pytest hooks\. Did you mean to use -p recording\?',
1731+
):
1732+
config = pytester.parseconfig(
1733+
"--disable-plugin-autoload", "-p", "pytest_recording"
1734+
)
1735+
1736+
assert config.pluginmanager.get_plugin("pytest_recording") is not None
1737+
1738+
16931739
def test_plugin_loading_order(pytester: Pytester) -> None:
16941740
"""Test order of plugin loading with `-p`."""
16951741
p1 = pytester.makepyfile(

0 commit comments

Comments
 (0)