diff --git a/changelog/804.bugfix.rst b/changelog/804.bugfix.rst new file mode 100644 index 00000000..f9a9edc7 --- /dev/null +++ b/changelog/804.bugfix.rst @@ -0,0 +1 @@ +:meth:`PluginManager.load_setuptools_entrypoints() ` no longer counts, or records the distribution of, an entry point whose plugin was not registered: a plugin which loads to ``None``, or one which a :class:`~pluggy.PluginManager` subclass declines to register, as pytest's does for plugins whose names were merged into ``pytest``. With such a plugin installed, listing plugins through :meth:`~pluggy.PluginManager.list_plugin_distinfo` and :meth:`~pluggy.PluginManager.list_plugin_distributions` raised :exc:`KeyError`. diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index 2697705b..c763682d 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -517,8 +517,16 @@ def load_setuptools_entrypoints(self, group: str, name: str | None = None) -> in ): continue plugin = ep.load() - self.register(plugin, name=ep.name) - self._name2dist[ep.name] = dist + registered_name = self.register(plugin, name=ep.name) + if registered_name is None: + # register() declined to register this plugin: it returns + # None for a None plugin, and a PluginManager subclass may + # do the same (pytest's ignores deprecated plugin names). + # Nothing was registered, so nothing may be counted or + # remembered -- list_plugin_distributions() looks the names + # up in the plugin registry. + continue + self._name2dist[registered_name] = dist count += 1 return count diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 7de6c6de..9382a088 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -993,6 +993,49 @@ class Distribution: assert pm.list_plugin_distributions() == [(a, dist)] +def test_load_setuptools_declined_plugin_is_not_recorded( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An entry point whose plugin ``register()`` declines was not loaded. + + ``register()`` returns ``None`` when it registers nothing: for a ``None`` + plugin, and for a ``PluginManager`` subclass which declines one, as + pytest's does for deprecated plugin names. Such an entry point must not be + counted, and must not be remembered for + :meth:`~pluggy.PluginManager.list_plugin_distinfo` and + :meth:`~pluggy.PluginManager.list_plugin_distributions`, which look the + recorded names up in the plugin registry. + """ + + class EntryPoint: + group = "hello" + + def __init__(self, name: str, target: object) -> None: + self.name = name + self._target = target + + def load(self) -> object: + return self._target + + class Distribution: + entry_points = (EntryPoint("null", None), EntryPoint("declined", object())) + + dist = cast(importlib.metadata.Distribution, Distribution()) + monkeypatch.setattr(importlib.metadata, "distributions", lambda: (dist,)) + + class DecliningPluginManager(PluginManager): + def register(self, plugin: object, name: str | None = None) -> str | None: + if name == "declined": + return None + return super().register(plugin, name=name) + + pm = DecliningPluginManager("example") + assert pm.load_setuptools_entrypoints("hello") == 0 + assert pm.list_name_plugin() == [] + assert pm.list_plugin_distributions() == [] + assert pm.list_plugin_distinfo() == [] + + def test_add_tracefuncs(he_pm: PluginManager) -> None: out: list[Any] = []