Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/pluggy/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Compatibility layer for legacy setuptools/pkg_resources API.

This module provides backward compatibility wrappers around modern
importlib.metadata, allowing gradual migration away from setuptools.
"""

from __future__ import annotations

import importlib.metadata
from typing import Any


class DistFacade:
"""Facade providing pkg_resources.Distribution-like interface.

This class wraps importlib.metadata.Distribution to provide a
compatibility layer for code expecting the legacy pkg_resources API.
The primary difference is the ``project_name`` attribute which
pkg_resources provided but importlib.metadata.Distribution does not.
"""

__slots__ = ("_dist",)

def __init__(self, dist: importlib.metadata.Distribution) -> None:
self._dist = dist

@property
def project_name(self) -> str:
"""Get the project name (for pkg_resources compatibility).

This is equivalent to dist.metadata["name"] but matches the
pkg_resources.Distribution.project_name attribute.
"""
name: str = self.metadata["name"]
return name

def __getattr__(self, attr: str) -> Any:
"""Delegate all other attributes to the wrapped Distribution."""
return getattr(self._dist, attr)

def __dir__(self) -> list[str]:
"""List available attributes including facade additions."""
return sorted(dir(self._dist) + ["_dist", "project_name"])

def __eq__(self, other: object) -> bool:
"""Compare DistFacade instances by their wrapped Distribution."""
if isinstance(other, DistFacade):
return self._dist == other._dist
return NotImplemented
49 changes: 27 additions & 22 deletions src/pluggy/_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@


if TYPE_CHECKING:
# importtlib.metadata import is slow, defer it.
import importlib.metadata

from ._compat import DistFacade


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


class DistFacade:
"""Emulate a pkg_resources Distribution"""

def __init__(self, dist: importlib.metadata.Distribution) -> None:
self._dist = dist

@property
def project_name(self) -> str:
name: str = self.metadata["name"]
return name

def __getattr__(self, attr: str, default: Any | None = None) -> Any:
return getattr(self._dist, attr, default)

def __dir__(self) -> list[str]:
return sorted(dir(self._dist) + ["_dist", "project_name"])


class PluginManager:
"""Core class which manages registration of plugin objects and 1:N hook
calling.
Expand All @@ -101,7 +84,9 @@ def __init__(self, project_name: str) -> None:
#: The project name.
self.project_name: Final = project_name
self._name2plugin: Final[dict[str, _Plugin]] = {}
self._plugin_distinfo: Final[list[tuple[_Plugin, DistFacade]]] = []
self._plugin_distinfo: Final[
list[tuple[_Plugin, importlib.metadata.Distribution]]
] = []
#: The "hook relay", used to call a hook on all registered plugins.
#: See :ref:`calling`.
self.hook: Final = HookRelay()
Expand Down Expand Up @@ -418,13 +403,33 @@ def load_setuptools_entrypoints(self, group: str, name: str | None = None) -> in
continue
plugin = ep.load()
self.register(plugin, name=ep.name)
self._plugin_distinfo.append((plugin, DistFacade(dist)))
self._plugin_distinfo.append((plugin, dist))
count += 1
return count

def list_plugin_distinfo(self) -> list[tuple[_Plugin, DistFacade]]:
"""Return a list of (plugin, distinfo) pairs for all
setuptools-registered plugins."""
setuptools-registered plugins.

.. note::
The distinfo objects are wrapped with :class:`~pluggy._compat.DistFacade`
for backward compatibility with the legacy pkg_resources API.
Use the modern :meth:`list_plugin_distributions` method to get
unwrapped :class:`importlib.metadata.Distribution` objects.
"""

from ._compat import DistFacade

return [(plugin, DistFacade(dist)) for plugin, dist in self._plugin_distinfo]

def list_plugin_distributions(
self,
) -> list[tuple[_Plugin, importlib.metadata.Distribution]]:
"""Return a list of (plugin, distribution) pairs for all plugins
loaded via entry points.

.. versionadded:: 1.7
"""
return list(self._plugin_distinfo)

def list_name_plugin(self) -> list[tuple[str, _Plugin]]:
Expand Down
2 changes: 1 addition & 1 deletion testing/test_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def myhook(self):


def test_dist_facade_list_attributes() -> None:
from pluggy._manager import DistFacade
from pluggy._compat import DistFacade

fc = DistFacade(distribution("pluggy"))
res = dir(fc)
Expand Down
19 changes: 12 additions & 7 deletions testing/test_pluginmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import importlib.metadata
from typing import Any
from typing import cast

import pytest

Expand All @@ -12,6 +13,7 @@
from pluggy import HookspecMarker
from pluggy import PluginManager
from pluggy import PluginValidationError
from pluggy._compat import DistFacade


hookspec = HookspecMarker("example")
Expand Down Expand Up @@ -583,7 +585,9 @@ class NoHooks:
pm.add_hookspecs(NoHooks)


def test_load_setuptools_instantiation(monkeypatch, pm: PluginManager) -> None:
def test_load_setuptools_instantiation(
monkeypatch: pytest.MonkeyPatch, pm: PluginManager
) -> None:
class EntryPoint:
name = "myname"
group = "hello"
Expand All @@ -598,7 +602,8 @@ class PseudoPlugin:
class Distribution:
entry_points = (EntryPoint(),)

dist = Distribution()
# Cast mock Distribution to satisfy mypy type checking
dist = cast(importlib.metadata.Distribution, Distribution())

def my_distributions():
return (dist,)
Expand All @@ -610,14 +615,14 @@ def my_distributions():
assert plugin is not None
assert plugin.x == 42
ret = pm.list_plugin_distinfo()
# poor man's `assert ret == [(plugin, mock.ANY)]`
assert len(ret) == 1
assert len(ret[0]) == 2
assert ret[0][0] == plugin
assert ret[0][1]._dist == dist # type: ignore[comparison-overlap]
assert ret == [(plugin, DistFacade(dist))]
num = pm.load_setuptools_entrypoints("hello")
assert num == 0 # no plugin loaded by this call

# Test the new modern API returns unwrapped Distribution objects
ret_modern = pm.list_plugin_distributions()
assert ret_modern == [(plugin, dist)]


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