Skip to content

Commit 86b5a28

Browse files
committed
Merge branch 'main' into feature/337-EntryPoint-object
2 parents 08bd99f + 0bc774f commit 86b5a28

File tree

12 files changed

+172
-51
lines changed

12 files changed

+172
-51
lines changed

CHANGES.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
v4.7.1
2+
======
3+
4+
* #344: Fixed regression in ``packages_distributions`` when
5+
neither top-level.txt nor a files manifest is present.
6+
7+
v4.7.0
8+
======
9+
10+
* #330: In ``packages_distributions``, now infer top-level
11+
names from ``.files()`` when a ``top-level.txt``
12+
(Setuptools-specific metadata) is not present.
13+
114
v4.6.4
215
======
316

docs/conf.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env python3
2-
# -*- coding: utf-8 -*-
32

43
extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker']
54

importlib_metadata/__init__.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
pypy_partial,
2323
)
2424
from ._functools import method_cache
25-
from ._itertools import unique_everseen
25+
from ._itertools import always_iterable, unique_everseen
2626
from ._meta import PackageMetadata, SimplePath
2727

2828
from contextlib import suppress
@@ -354,7 +354,7 @@ def names(self):
354354
"""
355355
Return the set of all names of all entry points.
356356
"""
357-
return set(ep.name for ep in self)
357+
return {ep.name for ep in self}
358358

359359
@property
360360
def groups(self):
@@ -365,7 +365,7 @@ def groups(self):
365365
>>> EntryPoints().groups
366366
set()
367367
"""
368-
return set(ep.group for ep in self)
368+
return {ep.group for ep in self}
369369

370370
@classmethod
371371
def _from_text_for(cls, text, dist):
@@ -1024,6 +1024,18 @@ def packages_distributions() -> Mapping[str, List[str]]:
10241024
"""
10251025
pkg_to_dist = collections.defaultdict(list)
10261026
for dist in distributions():
1027-
for pkg in (dist.read_text('top_level.txt') or '').split():
1027+
for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
10281028
pkg_to_dist[pkg].append(dist.metadata['Name'])
10291029
return dict(pkg_to_dist)
1030+
1031+
1032+
def _top_level_declared(dist):
1033+
return (dist.read_text('top_level.txt') or '').split()
1034+
1035+
1036+
def _top_level_inferred(dist):
1037+
return {
1038+
f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
1039+
for f in always_iterable(dist.files)
1040+
if f.suffix == ".py"
1041+
}

importlib_metadata/_itertools.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,57 @@ def unique_everseen(iterable, key=None):
1717
if k not in seen:
1818
seen_add(k)
1919
yield element
20+
21+
22+
# copied from more_itertools 8.8
23+
def always_iterable(obj, base_type=(str, bytes)):
24+
"""If *obj* is iterable, return an iterator over its items::
25+
26+
>>> obj = (1, 2, 3)
27+
>>> list(always_iterable(obj))
28+
[1, 2, 3]
29+
30+
If *obj* is not iterable, return a one-item iterable containing *obj*::
31+
32+
>>> obj = 1
33+
>>> list(always_iterable(obj))
34+
[1]
35+
36+
If *obj* is ``None``, return an empty iterable:
37+
38+
>>> obj = None
39+
>>> list(always_iterable(None))
40+
[]
41+
42+
By default, binary and text strings are not considered iterable::
43+
44+
>>> obj = 'foo'
45+
>>> list(always_iterable(obj))
46+
['foo']
47+
48+
If *base_type* is set, objects for which ``isinstance(obj, base_type)``
49+
returns ``True`` won't be considered iterable.
50+
51+
>>> obj = {'a': 1}
52+
>>> list(always_iterable(obj)) # Iterate over the dict's keys
53+
['a']
54+
>>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
55+
[{'a': 1}]
56+
57+
Set *base_type* to ``None`` to avoid any special handling and treat objects
58+
Python considers iterable as iterable:
59+
60+
>>> obj = 'foo'
61+
>>> list(always_iterable(obj, base_type=None))
62+
['f', 'o', 'o']
63+
"""
64+
if obj is None:
65+
return iter(())
66+
67+
if (base_type is not None) and isinstance(obj, base_type):
68+
return iter((obj,))
69+
70+
try:
71+
return iter(obj)
72+
except TypeError:
73+
return iter((obj,))

importlib_metadata/_text.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def __hash__(self):
8080
return hash(self.lower())
8181

8282
def __contains__(self, other):
83-
return super(FoldedCase, self).lower().__contains__(other.lower())
83+
return super().lower().__contains__(other.lower())
8484

8585
def in_(self, other):
8686
"Does self appear in other?"
@@ -89,7 +89,7 @@ def in_(self, other):
8989
# cache lower since it's likely to be called frequently.
9090
@method_cache
9191
def lower(self):
92-
return super(FoldedCase, self).lower()
92+
return super().lower()
9393

9494
def index(self, sub):
9595
return self.lower().index(sub.lower())

prepare/example2/example2/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def main():
2+
return "example"

prepare/example2/pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[build-system]
2+
build-backend = 'trampolim'
3+
requires = ['trampolim']
4+
5+
[project]
6+
name = 'example2'
7+
version = '1.0.0'
8+
9+
[project.scripts]
10+
example = 'example2:main'
1.14 KB
Binary file not shown.

tests/fixtures.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
from .py39compat import FS_NONASCII
1111
from typing import Dict, Union
1212

13+
try:
14+
from importlib import resources
15+
16+
getattr(resources, 'files')
17+
getattr(resources, 'as_file')
18+
except (ImportError, AttributeError):
19+
import importlib_resources as resources # type: ignore
20+
1321

1422
@contextlib.contextmanager
1523
def tempdir():
@@ -54,7 +62,7 @@ def setUp(self):
5462

5563
class SiteDir(Fixtures):
5664
def setUp(self):
57-
super(SiteDir, self).setUp()
65+
super().setUp()
5866
self.site_dir = self.fixtures.enter_context(tempdir())
5967

6068

@@ -69,7 +77,7 @@ def add_sys_path(dir):
6977
sys.path.remove(str(dir))
7078

7179
def setUp(self):
72-
super(OnSysPath, self).setUp()
80+
super().setUp()
7381
self.fixtures.enter_context(self.add_sys_path(self.site_dir))
7482

7583

@@ -106,7 +114,7 @@ def main():
106114
}
107115

108116
def setUp(self):
109-
super(DistInfoPkg, self).setUp()
117+
super().setUp()
110118
build_files(DistInfoPkg.files, self.site_dir)
111119

112120
def make_uppercase(self):
@@ -131,7 +139,7 @@ class DistInfoPkgWithDot(OnSysPath, SiteDir):
131139
}
132140

133141
def setUp(self):
134-
super(DistInfoPkgWithDot, self).setUp()
142+
super().setUp()
135143
build_files(DistInfoPkgWithDot.files, self.site_dir)
136144

137145

@@ -152,13 +160,13 @@ class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
152160
}
153161

154162
def setUp(self):
155-
super(DistInfoPkgWithDotLegacy, self).setUp()
163+
super().setUp()
156164
build_files(DistInfoPkgWithDotLegacy.files, self.site_dir)
157165

158166

159167
class DistInfoPkgOffPath(SiteDir):
160168
def setUp(self):
161-
super(DistInfoPkgOffPath, self).setUp()
169+
super().setUp()
162170
build_files(DistInfoPkg.files, self.site_dir)
163171

164172

@@ -198,7 +206,7 @@ def main():
198206
}
199207

200208
def setUp(self):
201-
super(EggInfoPkg, self).setUp()
209+
super().setUp()
202210
build_files(EggInfoPkg.files, prefix=self.site_dir)
203211

204212

@@ -219,7 +227,7 @@ class EggInfoFile(OnSysPath, SiteDir):
219227
}
220228

221229
def setUp(self):
222-
super(EggInfoFile, self).setUp()
230+
super().setUp()
223231
build_files(EggInfoFile.files, prefix=self.site_dir)
224232

225233

@@ -285,3 +293,19 @@ def DALS(str):
285293
class NullFinder:
286294
def find_module(self, name):
287295
pass
296+
297+
298+
class ZipFixtures:
299+
root = 'tests.data'
300+
301+
def _fixture_on_path(self, filename):
302+
pkg_file = resources.files(self.root).joinpath(filename)
303+
file = self.resources.enter_context(resources.as_file(pkg_file))
304+
assert file.name.startswith('example'), file.name
305+
sys.path.insert(0, str(file))
306+
self.resources.callback(sys.path.pop, 0)
307+
308+
def setUp(self):
309+
# Add self.zip_name to the front of sys.path.
310+
self.resources = contextlib.ExitStack()
311+
self.addCleanup(self.resources.close)

tests/test_api.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,14 +206,8 @@ def _test_files(files):
206206
file.read_text()
207207

208208
def test_file_hash_repr(self):
209-
try:
210-
assertRegex = self.assertRegex
211-
except AttributeError:
212-
# Python 2
213-
assertRegex = self.assertRegexpMatches
214-
215209
util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0]
216-
assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
210+
self.assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
217211

218212
def test_files_dist_info(self):
219213
self._test_files(files('distinfo-pkg'))

tests/test_main.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
distributions,
1818
entry_points,
1919
metadata,
20+
packages_distributions,
2021
version,
2122
)
2223

@@ -208,7 +209,7 @@ class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase):
208209
site_dir = '/access-denied'
209210

210211
def setUp(self):
211-
super(InaccessibleSysPath, self).setUp()
212+
super().setUp()
212213
self.setUpPyfakefs()
213214
self.fs.create_dir(self.site_dir, perm_bits=000)
214215

@@ -222,7 +223,7 @@ def test_discovery(self):
222223

223224
class TestEntryPoints(unittest.TestCase):
224225
def __init__(self, *args):
225-
super(TestEntryPoints, self).__init__(*args)
226+
super().__init__(*args)
226227
self.ep = importlib_metadata.EntryPoint(
227228
name='name', value='value', group='group'
228229
)
@@ -284,3 +285,38 @@ def test_unicode_dir_on_sys_path(self):
284285
prefix=self.site_dir,
285286
)
286287
list(distributions())
288+
289+
290+
class PackagesDistributionsPrebuiltTest(fixtures.ZipFixtures, unittest.TestCase):
291+
def test_packages_distributions_example(self):
292+
self._fixture_on_path('example-21.12-py3-none-any.whl')
293+
assert packages_distributions()['example'] == ['example']
294+
295+
def test_packages_distributions_example2(self):
296+
"""
297+
Test packages_distributions on a wheel built
298+
by trampolim.
299+
"""
300+
self._fixture_on_path('example2-1.0.0-py3-none-any.whl')
301+
assert packages_distributions()['example2'] == ['example2']
302+
303+
304+
class PackagesDistributionsTest(
305+
fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase
306+
):
307+
def test_packages_distributions_neither_toplevel_nor_files(self):
308+
"""
309+
Test a package built without 'top-level.txt' or a file list.
310+
"""
311+
fixtures.build_files(
312+
{
313+
'trim_example-1.0.0.dist-info': {
314+
'METADATA': """
315+
Name: trim_example
316+
Version: 1.0.0
317+
""",
318+
}
319+
},
320+
prefix=self.site_dir,
321+
)
322+
packages_distributions()

tests/test_zip.py

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22
import unittest
33

4-
from contextlib import ExitStack
4+
from . import fixtures
55
from importlib_metadata import (
66
PackageNotFoundError,
77
distribution,
@@ -11,30 +11,10 @@
1111
version,
1212
)
1313

14-
try:
15-
from importlib import resources
16-
17-
getattr(resources, 'files')
18-
getattr(resources, 'as_file')
19-
except (ImportError, AttributeError):
20-
import importlib_resources as resources # type: ignore
21-
22-
23-
class TestZip(unittest.TestCase):
24-
root = 'tests.data'
25-
26-
def _fixture_on_path(self, filename):
27-
pkg_file = resources.files(self.root).joinpath(filename)
28-
file = self.resources.enter_context(resources.as_file(pkg_file))
29-
assert file.name.startswith('example-'), file.name
30-
sys.path.insert(0, str(file))
31-
self.resources.callback(sys.path.pop, 0)
3214

15+
class TestZip(fixtures.ZipFixtures, unittest.TestCase):
3316
def setUp(self):
34-
# Find the path to the example-*.whl so we can add it to the front of
35-
# sys.path, where we'll then try to find the metadata thereof.
36-
self.resources = ExitStack()
37-
self.addCleanup(self.resources.close)
17+
super().setUp()
3818
self._fixture_on_path('example-21.12-py3-none-any.whl')
3919

4020
def test_zip_version(self):
@@ -69,10 +49,7 @@ def test_one_distribution(self):
6949

7050
class TestEgg(TestZip):
7151
def setUp(self):
72-
# Find the path to the example-*.egg so we can add it to the front of
73-
# sys.path, where we'll then try to find the metadata thereof.
74-
self.resources = ExitStack()
75-
self.addCleanup(self.resources.close)
52+
super().setUp()
7653
self._fixture_on_path('example-21.12-py3.6.egg')
7754

7855
def test_files(self):

0 commit comments

Comments
 (0)