Skip to content

Commit 735edb0

Browse files
Merge pull request #617 from RonnyPfannschmidt/refactor/setuptools-compat-module
Refactor setuptools compatibility and add plugin distribution tests
2 parents cba86ad + 53eeddf commit 735edb0

5 files changed

Lines changed: 108 additions & 28 deletions

File tree

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
# Don't want to expose this yet (see #428).
6868
("py:class", "pluggy._tracing.TagTracerSub"),
6969
# Compat hack, don't want to expose it.
70-
("py:class", "pluggy._manager.DistFacade"),
70+
("py:class", "pluggy._compat.DistFacade"),
7171
# `types.ModuleType` turns into `module` but then fails to resolve...
7272
("py:class", "module"),
7373
# Just a TypeVar.

src/pluggy/_compat.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
Compatibility layer for legacy setuptools/pkg_resources API.
3+
4+
This module provides backward compatibility wrappers around modern
5+
importlib.metadata, allowing gradual migration away from setuptools.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import importlib.metadata
11+
from typing import Any
12+
13+
14+
class DistFacade:
15+
"""Facade providing pkg_resources.Distribution-like interface.
16+
17+
This class wraps importlib.metadata.Distribution to provide a
18+
compatibility layer for code expecting the legacy pkg_resources API.
19+
The primary difference is the ``project_name`` attribute which
20+
pkg_resources provided but importlib.metadata.Distribution does not.
21+
"""
22+
23+
__slots__ = ("_dist",)
24+
25+
def __init__(self, dist: importlib.metadata.Distribution) -> None:
26+
self._dist = dist
27+
28+
@property
29+
def project_name(self) -> str:
30+
"""Get the project name (for pkg_resources compatibility).
31+
32+
This is equivalent to dist.metadata["name"] but matches the
33+
pkg_resources.Distribution.project_name attribute.
34+
"""
35+
name: str = self.metadata["name"]
36+
return name
37+
38+
def __getattr__(self, attr: str) -> Any:
39+
"""Delegate all other attributes to the wrapped Distribution."""
40+
return getattr(self._dist, attr)
41+
42+
def __dir__(self) -> list[str]:
43+
"""List available attributes including facade additions."""
44+
return sorted(dir(self._dist) + ["_dist", "project_name"])

src/pluggy/_manager.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@
2929

3030

3131
if TYPE_CHECKING:
32-
# importtlib.metadata import is slow, defer it.
3332
import importlib.metadata
3433

34+
from ._compat import DistFacade
35+
3536

3637
_BeforeTrace: TypeAlias = Callable[[str, Sequence[HookImpl], Mapping[str, Any]], None]
3738
_AfterTrace: TypeAlias = Callable[
@@ -62,24 +63,6 @@ def __init__(self, plugin: _Plugin, message: str) -> None:
6263
self.plugin = plugin
6364

6465

65-
class DistFacade:
66-
"""Emulate a pkg_resources Distribution"""
67-
68-
def __init__(self, dist: importlib.metadata.Distribution) -> None:
69-
self._dist = dist
70-
71-
@property
72-
def project_name(self) -> str:
73-
name: str = self.metadata["name"]
74-
return name
75-
76-
def __getattr__(self, attr: str, default: Any | None = None) -> Any:
77-
return getattr(self._dist, attr, default)
78-
79-
def __dir__(self) -> list[str]:
80-
return sorted(dir(self._dist) + ["_dist", "project_name"])
81-
82-
8366
class PluginManager:
8467
"""Core class which manages registration of plugin objects and 1:N hook
8568
calling.
@@ -101,7 +84,9 @@ def __init__(self, project_name: str) -> None:
10184
#: The project name.
10285
self.project_name: Final = project_name
10386
self._name2plugin: Final[dict[str, _Plugin]] = {}
104-
self._plugin_distinfo: Final[list[tuple[_Plugin, DistFacade]]] = []
87+
self._plugin_distinfo: Final[
88+
list[tuple[_Plugin, importlib.metadata.Distribution]]
89+
] = []
10590
#: The "hook relay", used to call a hook on all registered plugins.
10691
#: See :ref:`calling`.
10792
self.hook: Final = HookRelay()
@@ -418,13 +403,32 @@ def load_setuptools_entrypoints(self, group: str, name: str | None = None) -> in
418403
continue
419404
plugin = ep.load()
420405
self.register(plugin, name=ep.name)
421-
self._plugin_distinfo.append((plugin, DistFacade(dist)))
406+
self._plugin_distinfo.append((plugin, dist))
422407
count += 1
423408
return count
424409

425410
def list_plugin_distinfo(self) -> list[tuple[_Plugin, DistFacade]]:
426411
"""Return a list of (plugin, distinfo) pairs for all
427-
setuptools-registered plugins."""
412+
setuptools-registered plugins.
413+
414+
.. note::
415+
The distinfo objects are wrapped with :class:`~pluggy._compat.DistFacade`
416+
for backward compatibility with the legacy pkg_resources API.
417+
Prefer :meth:`list_plugin_distributions` for raw
418+
:class:`importlib.metadata.Distribution` objects.
419+
"""
420+
from ._compat import DistFacade
421+
422+
return [(plugin, DistFacade(dist)) for plugin, dist in self._plugin_distinfo]
423+
424+
def list_plugin_distributions(
425+
self,
426+
) -> list[tuple[_Plugin, importlib.metadata.Distribution]]:
427+
"""Return a list of (plugin, distribution) pairs for all plugins
428+
loaded via entry points.
429+
430+
.. versionadded:: 1.7
431+
"""
428432
return list(self._plugin_distinfo)
429433

430434
def list_name_plugin(self) -> list[tuple[str, _Plugin]]:

testing/test_details.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,39 @@ def myhook(self):
197197

198198

199199
def test_dist_facade_list_attributes() -> None:
200-
from pluggy._manager import DistFacade
200+
from pluggy._compat import DistFacade
201201

202202
fc = DistFacade(distribution("pluggy"))
203203
res = dir(fc)
204204
assert res == sorted(res)
205205
assert set(res) - set(dir(fc._dist)) == {"_dist", "project_name"}
206206

207207

208+
def test_dist_facade_project_name_and_delegation() -> None:
209+
from pluggy._compat import DistFacade
210+
211+
dist = distribution("pluggy")
212+
fc = DistFacade(dist)
213+
assert fc.project_name == dist.metadata["name"]
214+
assert fc.version == dist.version
215+
assert fc._dist is dist
216+
with pytest.raises(AttributeError):
217+
_ = fc.definitely_missing_attribute
218+
219+
220+
def test_dist_facade_identity_equality_and_hash() -> None:
221+
from pluggy._compat import DistFacade
222+
223+
dist = distribution("pluggy")
224+
fc1 = DistFacade(dist)
225+
fc2 = DistFacade(dist)
226+
assert fc1 == fc1
227+
assert fc1 is not fc2
228+
assert fc1 != fc2
229+
assert hash(fc1) == hash(fc1)
230+
assert {fc1: "ok"}[fc1] == "ok"
231+
232+
208233
def test_hookimpl_disallow_invalid_combination() -> None:
209234
decorator = hookspec(historic=True, firstresult=True)
210235
with pytest.raises(ValueError, match="cannot have a historic firstresult hook"):

testing/test_pluginmanager.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import importlib.metadata
66
from typing import Any
7+
from typing import cast
78

89
import pytest
910

@@ -583,7 +584,9 @@ class NoHooks:
583584
pm.add_hookspecs(NoHooks)
584585

585586

586-
def test_load_setuptools_instantiation(monkeypatch, pm: PluginManager) -> None:
587+
def test_load_setuptools_instantiation(
588+
monkeypatch: pytest.MonkeyPatch, pm: PluginManager
589+
) -> None:
587590
class EntryPoint:
588591
name = "myname"
589592
group = "hello"
@@ -598,7 +601,8 @@ class PseudoPlugin:
598601
class Distribution:
599602
entry_points = (EntryPoint(),)
600603

601-
dist = Distribution()
604+
# Cast mock Distribution to satisfy mypy type checking
605+
dist = cast(importlib.metadata.Distribution, Distribution())
602606

603607
def my_distributions():
604608
return (dist,)
@@ -614,10 +618,13 @@ def my_distributions():
614618
assert len(ret) == 1
615619
assert len(ret[0]) == 2
616620
assert ret[0][0] == plugin
617-
assert ret[0][1]._dist == dist # type: ignore[comparison-overlap]
618-
num = pm.load_setuptools_entrypoints("hello") # type:ignore[unreachable]
621+
assert ret[0][1]._dist == dist
622+
num = pm.load_setuptools_entrypoints("hello")
619623
assert num == 0 # no plugin loaded by this call
620624

625+
ret_distributions = pm.list_plugin_distributions()
626+
assert ret_distributions == [(plugin, dist)]
627+
621628

622629
def test_add_tracefuncs(he_pm: PluginManager) -> None:
623630
out: list[Any] = []

0 commit comments

Comments
 (0)