Skip to content

Commit

Permalink
Merge pull request #397 from abravalheri/issue-396
Browse files Browse the repository at this point in the history
Add compatibility for `PathDistributions` implemented with stdlib objects in Python 3.8/3.9
  • Loading branch information
jaraco committed Oct 1, 2022
2 parents a676a67 + 25998e4 commit 6bee315
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Expand Up @@ -20,6 +20,10 @@ jobs:
include:
- python: pypy3.9
platform: ubuntu-latest
- platform: ubuntu-latest
python: "3.8"
- platform: ubuntu-latest
python: "3.9"
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
Expand Down
6 changes: 6 additions & 0 deletions CHANGES.rst
@@ -1,3 +1,9 @@
v4.13.0
=======

* #396: Added compatibility for ``PathDistributions`` originating
from Python 3.8 and 3.9.

v4.12.0
=======

Expand Down
11 changes: 8 additions & 3 deletions importlib_metadata/__init__.py
Expand Up @@ -14,7 +14,7 @@
import posixpath
import collections

from . import _adapters, _meta
from . import _adapters, _meta, _py39compat
from ._collections import FreezableDefaultDict, Pair
from ._compat import (
NullFinder,
Expand Down Expand Up @@ -189,6 +189,10 @@ class EntryPoint(DeprecatedTuple):
following the attr, and following any extras.
"""

name: str
value: str
group: str

dist: Optional['Distribution'] = None

def __init__(self, name, value, group):
Expand Down Expand Up @@ -378,7 +382,8 @@ def select(self, **params):
Select entry points from self that match the
given parameters (typically group and/or name).
"""
return EntryPoints(ep for ep in self if ep.matches(**params))
candidates = (_py39compat.ep_matches(ep, **params) for ep in self)
return EntryPoints(ep for ep, predicate in candidates if predicate)

@property
def names(self):
Expand Down Expand Up @@ -1017,7 +1022,7 @@ def version(distribution_name):

_unique = functools.partial(
unique_everseen,
key=operator.attrgetter('_normalized_name'),
key=_py39compat.normalized_name,
)
"""
Wrapper for ``distributions`` to return unique distributions by name.
Expand Down
48 changes: 48 additions & 0 deletions importlib_metadata/_py39compat.py
@@ -0,0 +1,48 @@
"""
Compatibility layer with Python 3.8/3.9
"""
from typing import TYPE_CHECKING, Any, Optional, Tuple

if TYPE_CHECKING: # pragma: no cover
# Prevent circular imports on runtime.
from . import Distribution, EntryPoint
else:
Distribution = EntryPoint = Any


def normalized_name(dist: Distribution) -> Optional[str]:
"""
Honor name normalization for distributions that don't provide ``_normalized_name``.
"""
try:
return dist._normalized_name
except AttributeError:
from . import Prepared # -> delay to prevent circular imports.

return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name'])


def ep_matches(ep: EntryPoint, **params) -> Tuple[EntryPoint, bool]:
"""
Workaround for ``EntryPoint`` objects without the ``matches`` method.
For the sake of convenience, a tuple is returned containing not only the
boolean value corresponding to the predicate evalutation, but also a compatible
``EntryPoint`` object that can be safely used at a later stage.
For example, the following sequences of expressions should be compatible:
# Sequence 1: using the compatibility layer
candidates = (_py39compat.ep_matches(ep, **params) for ep in entry_points)
[ep for ep, predicate in candidates if predicate]
# Sequence 2: using Python 3.9+
[ep for ep in entry_points if ep.matches(**params)]
"""
try:
return ep, ep.matches(**params)
except AttributeError:
from . import EntryPoint # -> delay to prevent circular imports.

# Reconstruct the EntryPoint object to make sure it is compatible.
_ep = EntryPoint(ep.name, ep.value, ep.group)
return _ep, _ep.matches(**params)
74 changes: 74 additions & 0 deletions tests/test_py39compat.py
@@ -0,0 +1,74 @@
import sys
import pathlib
import unittest

from . import fixtures
from importlib_metadata import (
distribution,
distributions,
entry_points,
metadata,
version,
)


class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
def setUp(self):
python_version = sys.version_info[:2]
if python_version < (3, 8) or python_version > (3, 9):
self.skipTest("Tests specific for Python 3.8/3.9")
super().setUp()

def _meta_path_finder(self):
from importlib.metadata import (
Distribution,
DistributionFinder,
PathDistribution,
)
from importlib.util import spec_from_file_location

path = pathlib.Path(self.site_dir)

class CustomDistribution(Distribution):
def __init__(self, name, path):
self.name = name
self._path_distribution = PathDistribution(path)

def read_text(self, filename):
return self._path_distribution.read_text(filename)

def locate_file(self, path):
return self._path_distribution.locate_file(path)

class CustomFinder:
@classmethod
def find_spec(cls, fullname, _path=None, _target=None):
candidate = pathlib.Path(path, *fullname.split(".")).with_suffix(".py")
if candidate.exists():
return spec_from_file_location(fullname, candidate)

@classmethod
def find_distributions(self, context=DistributionFinder.Context()):
for dist_info in path.glob("*.dist-info"):
yield PathDistribution(dist_info)
name, _, _ = str(dist_info).partition("-")
yield CustomDistribution(name + "_custom", dist_info)

return CustomFinder

def test_compatibility_with_old_stdlib_path_distribution(self):
"""
Given a custom finder that uses Python 3.8/3.9 importlib.metadata is installed,
when importlib_metadata functions are called, there should be no exceptions.
Ref python/importlib_metadata#396.
"""
self.fixtures.enter_context(fixtures.install_finder(self._meta_path_finder()))

assert list(distributions())
assert distribution("distinfo_pkg")
assert distribution("distinfo_pkg_custom")
assert version("distinfo_pkg") > "0"
assert version("distinfo_pkg_custom") > "0"
assert list(metadata("distinfo_pkg"))
assert list(metadata("distinfo_pkg_custom"))
assert list(entry_points(group="entries"))

0 comments on commit 6bee315

Please sign in to comment.