Skip to content

Commit 6bee315

Browse files
authored
Merge pull request #397 from abravalheri/issue-396
Add compatibility for `PathDistributions` implemented with stdlib objects in Python 3.8/3.9
2 parents a676a67 + 25998e4 commit 6bee315

File tree

5 files changed

+140
-3
lines changed

5 files changed

+140
-3
lines changed

.github/workflows/main.yml

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ jobs:
2020
include:
2121
- python: pypy3.9
2222
platform: ubuntu-latest
23+
- platform: ubuntu-latest
24+
python: "3.8"
25+
- platform: ubuntu-latest
26+
python: "3.9"
2327
runs-on: ${{ matrix.platform }}
2428
steps:
2529
- uses: actions/checkout@v3

CHANGES.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
v4.13.0
2+
=======
3+
4+
* #396: Added compatibility for ``PathDistributions`` originating
5+
from Python 3.8 and 3.9.
6+
17
v4.12.0
28
=======
39

importlib_metadata/__init__.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import posixpath
1515
import collections
1616

17-
from . import _adapters, _meta
17+
from . import _adapters, _meta, _py39compat
1818
from ._collections import FreezableDefaultDict, Pair
1919
from ._compat import (
2020
NullFinder,
@@ -189,6 +189,10 @@ class EntryPoint(DeprecatedTuple):
189189
following the attr, and following any extras.
190190
"""
191191

192+
name: str
193+
value: str
194+
group: str
195+
192196
dist: Optional['Distribution'] = None
193197

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

383388
@property
384389
def names(self):
@@ -1017,7 +1022,7 @@ def version(distribution_name):
10171022

10181023
_unique = functools.partial(
10191024
unique_everseen,
1020-
key=operator.attrgetter('_normalized_name'),
1025+
key=_py39compat.normalized_name,
10211026
)
10221027
"""
10231028
Wrapper for ``distributions`` to return unique distributions by name.

importlib_metadata/_py39compat.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
Compatibility layer with Python 3.8/3.9
3+
"""
4+
from typing import TYPE_CHECKING, Any, Optional, Tuple
5+
6+
if TYPE_CHECKING: # pragma: no cover
7+
# Prevent circular imports on runtime.
8+
from . import Distribution, EntryPoint
9+
else:
10+
Distribution = EntryPoint = Any
11+
12+
13+
def normalized_name(dist: Distribution) -> Optional[str]:
14+
"""
15+
Honor name normalization for distributions that don't provide ``_normalized_name``.
16+
"""
17+
try:
18+
return dist._normalized_name
19+
except AttributeError:
20+
from . import Prepared # -> delay to prevent circular imports.
21+
22+
return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name'])
23+
24+
25+
def ep_matches(ep: EntryPoint, **params) -> Tuple[EntryPoint, bool]:
26+
"""
27+
Workaround for ``EntryPoint`` objects without the ``matches`` method.
28+
For the sake of convenience, a tuple is returned containing not only the
29+
boolean value corresponding to the predicate evalutation, but also a compatible
30+
``EntryPoint`` object that can be safely used at a later stage.
31+
32+
For example, the following sequences of expressions should be compatible:
33+
34+
# Sequence 1: using the compatibility layer
35+
candidates = (_py39compat.ep_matches(ep, **params) for ep in entry_points)
36+
[ep for ep, predicate in candidates if predicate]
37+
38+
# Sequence 2: using Python 3.9+
39+
[ep for ep in entry_points if ep.matches(**params)]
40+
"""
41+
try:
42+
return ep, ep.matches(**params)
43+
except AttributeError:
44+
from . import EntryPoint # -> delay to prevent circular imports.
45+
46+
# Reconstruct the EntryPoint object to make sure it is compatible.
47+
_ep = EntryPoint(ep.name, ep.value, ep.group)
48+
return _ep, _ep.matches(**params)

tests/test_py39compat.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import sys
2+
import pathlib
3+
import unittest
4+
5+
from . import fixtures
6+
from importlib_metadata import (
7+
distribution,
8+
distributions,
9+
entry_points,
10+
metadata,
11+
version,
12+
)
13+
14+
15+
class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
16+
def setUp(self):
17+
python_version = sys.version_info[:2]
18+
if python_version < (3, 8) or python_version > (3, 9):
19+
self.skipTest("Tests specific for Python 3.8/3.9")
20+
super().setUp()
21+
22+
def _meta_path_finder(self):
23+
from importlib.metadata import (
24+
Distribution,
25+
DistributionFinder,
26+
PathDistribution,
27+
)
28+
from importlib.util import spec_from_file_location
29+
30+
path = pathlib.Path(self.site_dir)
31+
32+
class CustomDistribution(Distribution):
33+
def __init__(self, name, path):
34+
self.name = name
35+
self._path_distribution = PathDistribution(path)
36+
37+
def read_text(self, filename):
38+
return self._path_distribution.read_text(filename)
39+
40+
def locate_file(self, path):
41+
return self._path_distribution.locate_file(path)
42+
43+
class CustomFinder:
44+
@classmethod
45+
def find_spec(cls, fullname, _path=None, _target=None):
46+
candidate = pathlib.Path(path, *fullname.split(".")).with_suffix(".py")
47+
if candidate.exists():
48+
return spec_from_file_location(fullname, candidate)
49+
50+
@classmethod
51+
def find_distributions(self, context=DistributionFinder.Context()):
52+
for dist_info in path.glob("*.dist-info"):
53+
yield PathDistribution(dist_info)
54+
name, _, _ = str(dist_info).partition("-")
55+
yield CustomDistribution(name + "_custom", dist_info)
56+
57+
return CustomFinder
58+
59+
def test_compatibility_with_old_stdlib_path_distribution(self):
60+
"""
61+
Given a custom finder that uses Python 3.8/3.9 importlib.metadata is installed,
62+
when importlib_metadata functions are called, there should be no exceptions.
63+
Ref python/importlib_metadata#396.
64+
"""
65+
self.fixtures.enter_context(fixtures.install_finder(self._meta_path_finder()))
66+
67+
assert list(distributions())
68+
assert distribution("distinfo_pkg")
69+
assert distribution("distinfo_pkg_custom")
70+
assert version("distinfo_pkg") > "0"
71+
assert version("distinfo_pkg_custom") > "0"
72+
assert list(metadata("distinfo_pkg"))
73+
assert list(metadata("distinfo_pkg_custom"))
74+
assert list(entry_points(group="entries"))

0 commit comments

Comments
 (0)