From 788c103d8aadf2ee314751b113b018ac36a12a53 Mon Sep 17 00:00:00 2001 From: Ant & Iren Liberty Studio Date: Wed, 14 Oct 2026 21:30:47 +0200 Subject: [PATCH] Remove stale plugin distribution entries on unregister 'load_setuptools_entrypoints()' appends to '_plugin_distinfo', but 'unregister()' only removed the plugin from '_name2plugin' and the hook callers. As a result 'list_plugin_distinfo()' and 'list_plugin_distributions()' kept reporting an unregistered plugin, and re-registering it added a duplicate entry. 'unregister()' now also drops the plugin's '_plugin_distinfo' entries. Fixes #763 --- changelog/763.bugfix.rst | 5 ++++ src/pluggy/_manager.py | 7 ++++++ testing/test_pluginmanager.py | 44 +++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 changelog/763.bugfix.rst diff --git a/changelog/763.bugfix.rst b/changelog/763.bugfix.rst new file mode 100644 index 00000000..e6e1e5a2 --- /dev/null +++ b/changelog/763.bugfix.rst @@ -0,0 +1,5 @@ +``PluginManager.unregister()`` now removes the plugin from +``list_plugin_distinfo()`` and ``list_plugin_distributions()``. Previously +these kept reporting an unregistered plugin loaded via +``load_setuptools_entrypoints()``, and re-registering it added a duplicate +entry. diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index aa54088c..55f40358 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -320,6 +320,13 @@ def unregister( if self._name2plugin.get(name) is not None: del self._name2plugin[name] + # Drop any setuptools distribution bookkeeping for this plugin so it + # no longer shows up in list_plugin_distinfo()/list_plugin_distributions() + # and a later re-registration does not create duplicate entries. + self._plugin_distinfo[:] = [ + (p, dist) for (p, dist) in self._plugin_distinfo if p is not plugin + ] + return plugin def set_blocked(self, name: str) -> None: diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 96ce5603..346efccf 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -959,6 +959,50 @@ def my_distributions(): assert ret_distributions == [(plugin, dist)] +def test_unregister_removes_plugin_distinfo( + monkeypatch: pytest.MonkeyPatch, pm: PluginManager +) -> None: + """Unregistering a setuptools-loaded plugin clears its distinfo entries. + + See https://github.com/pytest-dev/pluggy/issues/763. + """ + + class EntryPoint: + name = "myname" + group = "hello" + value = "myname:foo" + + def load(self): + class PseudoPlugin: + pass + + return PseudoPlugin() + + class Distribution: + entry_points = (EntryPoint(),) + + dist = cast(importlib.metadata.Distribution, Distribution()) + + monkeypatch.setattr(importlib.metadata, "distributions", lambda: (dist,)) + + assert pm.load_setuptools_entrypoints("hello") == 1 + plugin = pm.get_plugin("myname") + assert plugin is not None + assert len(pm.list_plugin_distinfo()) == 1 + assert len(pm.list_plugin_distributions()) == 1 + + # Unregistering must drop both distinfo views. + pm.unregister(name="myname") + assert pm.get_plugin("myname") is None + assert pm.list_plugin_distinfo() == [] + assert pm.list_plugin_distributions() == [] + + # Re-registration must not leave duplicate distinfo entries behind. + assert pm.load_setuptools_entrypoints("hello") == 1 + assert len(pm.list_plugin_distinfo()) == 1 + assert len(pm.list_plugin_distributions()) == 1 + + def test_add_tracefuncs(he_pm: PluginManager) -> None: out: list[Any] = []