From 9d13dec588cf7d947fb02a7f44c1d8dc846a193d Mon Sep 17 00:00:00 2001 From: simpleqt <2015900818@qq.com> Date: Tue, 20 Oct 2026 18:14:43 +0800 Subject: [PATCH] fix: don't count or record entry points that register() declined load_setuptools_entrypoints() recorded the distribution of every entry point it found and counted it as loaded, without looking at what register() did. register() registers nothing and returns None for a None plugin, and a PluginManager subclass may decline as well: pytest's declines the names of plugins which were merged into pytest (pytest_faulthandler, pytest_catchlog, ...). The recorded distributions are keyed by plugin name (#763), so list_plugin_distributions() looks those names up in the plugin registry, and list_plugin_distinfo() calls it: for a declined but recorded entry point both raised KeyError. With pytest-faulthandler installed, a plain pytest run died with "INTERNALERROR ... KeyError: 'pytest_faulthandler'" while printing the header, which lists plugins through list_plugin_distinfo(). Skip the bookkeeping when register() reports no registered name, and key the distribution under the name register() returned. Tests: new test_load_setuptools_declined_plugin_is_not_recorded covers a None plugin and a manager subclass which declines one, and fails on the previous code with "assert 2 == 0". Signed-off-by: simpleqt <2015900818@qq.com> --- changelog/804.bugfix.rst | 1 + src/pluggy/_manager.py | 12 ++++++++-- testing/test_pluginmanager.py | 43 +++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 changelog/804.bugfix.rst 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] = []