Skip to content

Commit 7ccc3b2

Browse files
committed
gh-93259: Validate arg to Distribution.from_name.
Syncs with importlib_metadata 4.12.0.
1 parent bec802d commit 7ccc3b2

File tree

6 files changed

+133
-64
lines changed

6 files changed

+133
-64
lines changed

Doc/library/importlib.metadata.rst

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
package metadata. Built in part on Python's import system, this library
1818
intends to replace similar functionality in the `entry point
1919
API`_ and `metadata API`_ of ``pkg_resources``. Along with
20-
:mod:`importlib.resources` (with new features backported to the
21-
`importlib_resources`_ package), this can eliminate the need to use the older
20+
:mod:`importlib.resources` (with new features backported to
21+
:doc:`importlib_resources <importlib_resources:index>`),
22+
this package can eliminate the need to use the older
2223
and less efficient
2324
``pkg_resources`` package.
2425

@@ -32,6 +33,13 @@ By default, package metadata can live on the file system or in zip archives on
3233
anywhere.
3334

3435

36+
.. seealso::
37+
38+
https://importlib-metadata.readthedocs.io/
39+
The documentation for ``importlib_metadata``, which supplies a
40+
backport of ``importlib.metadata``.
41+
42+
3543
Overview
3644
========
3745

@@ -54,9 +62,9 @@ You can get the version string for ``wheel`` by running the following:
5462
>>> version('wheel') # doctest: +SKIP
5563
'0.32.3'
5664
57-
You can also get the set of entry points keyed by group, such as
65+
You can also get a collection of entry points selectable by properties of the EntryPoint (typically 'group' or 'name'), such as
5866
``console_scripts``, ``distutils.commands`` and others. Each group contains a
59-
sequence of :ref:`EntryPoint <entry-points>` objects.
67+
collection of :ref:`EntryPoint <entry-points>` objects.
6068

6169
You can get the :ref:`metadata for a distribution <metadata>`::
6270

@@ -91,7 +99,7 @@ Query all entry points::
9199
>>> eps = entry_points() # doctest: +SKIP
92100

93101
The ``entry_points()`` function returns an ``EntryPoints`` object,
94-
a sequence of all ``EntryPoint`` objects with ``names`` and ``groups``
102+
a collection of all ``EntryPoint`` objects with ``names`` and ``groups``
95103
attributes for convenience::
96104

97105
>>> sorted(eps.groups) # doctest: +SKIP
@@ -174,6 +182,13 @@ all the metadata in a JSON-compatible form per :PEP:`566`::
174182
>>> wheel_metadata.json['requires_python']
175183
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
176184

185+
.. note::
186+
187+
The actual type of the object returned by ``metadata()`` is an
188+
implementation detail and should be accessed only through the interface
189+
described by the
190+
`PackageMetadata protocol <https://importlib-metadata.readthedocs.io/en/latest/api.html#importlib_metadata.PackageMetadata>`.
191+
177192
.. versionchanged:: 3.10
178193
The ``Description`` is now included in the metadata when presented
179194
through the payload. Line continuation characters have been removed.
@@ -295,6 +310,15 @@ The full set of available metadata is not described here. See :pep:`566`
295310
for additional details.
296311

297312

313+
Distribution Discovery
314+
======================
315+
316+
By default, this package provides built-in support for discovery of metadata for file system and zip file packages. This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular:
317+
318+
- ``importlib.metadata`` does not honor :class:`bytes` objects on ``sys.path``.
319+
- ``importlib.metadata`` will incidentally honor :py:class:`pathlib.Path` objects on ``sys.path`` even though such values will be ignored for imports.
320+
321+
298322
Extending the search algorithm
299323
==============================
300324

Lib/importlib/metadata/__init__.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -543,21 +543,21 @@ def locate_file(self, path):
543543
"""
544544

545545
@classmethod
546-
def from_name(cls, name):
546+
def from_name(cls, name: str):
547547
"""Return the Distribution for the given package name.
548548
549549
:param name: The name of the distribution package to search for.
550550
:return: The Distribution instance (or subclass thereof) for the named
551551
package, if found.
552552
:raises PackageNotFoundError: When the named package's distribution
553553
metadata cannot be found.
554+
:raises ValueError: When an invalid value is supplied for name.
554555
"""
555-
for resolver in cls._discover_resolvers():
556-
dists = resolver(DistributionFinder.Context(name=name))
557-
dist = next(iter(dists), None)
558-
if dist is not None:
559-
return dist
560-
else:
556+
if not name:
557+
raise ValueError("A distribution name is required.")
558+
try:
559+
return next(cls.discover(name=name))
560+
except StopIteration:
561561
raise PackageNotFoundError(name)
562562

563563
@classmethod
@@ -945,13 +945,26 @@ def _normalized_name(self):
945945
normalized name from the file system path.
946946
"""
947947
stem = os.path.basename(str(self._path))
948-
return self._name_from_stem(stem) or super()._normalized_name
948+
return (
949+
pass_none(Prepared.normalize)(self._name_from_stem(stem))
950+
or super()._normalized_name
951+
)
949952

950-
def _name_from_stem(self, stem):
951-
name, ext = os.path.splitext(stem)
953+
@staticmethod
954+
def _name_from_stem(stem):
955+
"""
956+
>>> PathDistribution._name_from_stem('foo-3.0.egg-info')
957+
'foo'
958+
>>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info')
959+
'CherryPy'
960+
>>> PathDistribution._name_from_stem('face.egg-info')
961+
'face'
962+
>>> PathDistribution._name_from_stem('foo.bar')
963+
"""
964+
filename, ext = os.path.splitext(stem)
952965
if ext not in ('.dist-info', '.egg-info'):
953966
return
954-
name, sep, rest = stem.partition('-')
967+
name, sep, rest = filename.partition('-')
955968
return name
956969

957970

@@ -991,6 +1004,15 @@ def version(distribution_name):
9911004
return distribution(distribution_name).version
9921005

9931006

1007+
_unique = functools.partial(
1008+
unique_everseen,
1009+
key=operator.attrgetter('_normalized_name'),
1010+
)
1011+
"""
1012+
Wrapper for ``distributions`` to return unique distributions by name.
1013+
"""
1014+
1015+
9941016
def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
9951017
"""Return EntryPoint objects for all installed packages.
9961018
@@ -1008,10 +1030,8 @@ def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
10081030
10091031
:return: EntryPoints or SelectableGroups for all installed packages.
10101032
"""
1011-
norm_name = operator.attrgetter('_normalized_name')
1012-
unique = functools.partial(unique_everseen, key=norm_name)
10131033
eps = itertools.chain.from_iterable(
1014-
dist.entry_points for dist in unique(distributions())
1034+
dist.entry_points for dist in _unique(distributions())
10151035
)
10161036
return SelectableGroups.load(eps).select(**params)
10171037

Lib/test/test_importlib/fixtures.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pathlib
66
import tempfile
77
import textwrap
8+
import functools
89
import contextlib
910

1011
from test.support.os_helper import FS_NONASCII
@@ -296,3 +297,18 @@ def setUp(self):
296297
# Add self.zip_name to the front of sys.path.
297298
self.resources = contextlib.ExitStack()
298299
self.addCleanup(self.resources.close)
300+
301+
302+
def parameterize(*args_set):
303+
"""Run test method with a series of parameters."""
304+
305+
def wrapper(func):
306+
@functools.wraps(func)
307+
def _inner(self):
308+
for args in args_set:
309+
with self.subTest(**args):
310+
func(self, **args)
311+
312+
return _inner
313+
314+
return wrapper

Lib/test/test_importlib/test_main.py

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import re
22
import json
33
import pickle
4-
import textwrap
54
import unittest
65
import warnings
76
import importlib.metadata
@@ -16,6 +15,7 @@
1615
Distribution,
1716
EntryPoint,
1817
PackageNotFoundError,
18+
_unique,
1919
distributions,
2020
entry_points,
2121
metadata,
@@ -51,6 +51,14 @@ def test_package_not_found_mentions_metadata(self):
5151
def test_new_style_classes(self):
5252
self.assertIsInstance(Distribution, type)
5353

54+
@fixtures.parameterize(
55+
dict(name=None),
56+
dict(name=''),
57+
)
58+
def test_invalid_inputs_to_from_name(self, name):
59+
with self.assertRaises(Exception):
60+
Distribution.from_name(name)
61+
5462

5563
class ImportTests(fixtures.DistInfoPkg, unittest.TestCase):
5664
def test_import_nonexistent_module(self):
@@ -78,48 +86,50 @@ def test_resolve_without_attr(self):
7886

7987
class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
8088
@staticmethod
81-
def pkg_with_dashes(site_dir):
89+
def make_pkg(name):
8290
"""
83-
Create minimal metadata for a package with dashes
84-
in the name (and thus underscores in the filename).
91+
Create minimal metadata for a dist-info package with
92+
the indicated name on the file system.
8593
"""
86-
metadata_dir = site_dir / 'my_pkg.dist-info'
87-
metadata_dir.mkdir()
88-
metadata = metadata_dir / 'METADATA'
89-
with metadata.open('w', encoding='utf-8') as strm:
90-
strm.write('Version: 1.0\n')
91-
return 'my-pkg'
94+
return {
95+
f'{name}.dist-info': {
96+
'METADATA': 'VERSION: 1.0\n',
97+
},
98+
}
9299

93100
def test_dashes_in_dist_name_found_as_underscores(self):
94101
"""
95102
For a package with a dash in the name, the dist-info metadata
96103
uses underscores in the name. Ensure the metadata loads.
97104
"""
98-
pkg_name = self.pkg_with_dashes(self.site_dir)
99-
assert version(pkg_name) == '1.0'
100-
101-
@staticmethod
102-
def pkg_with_mixed_case(site_dir):
103-
"""
104-
Create minimal metadata for a package with mixed case
105-
in the name.
106-
"""
107-
metadata_dir = site_dir / 'CherryPy.dist-info'
108-
metadata_dir.mkdir()
109-
metadata = metadata_dir / 'METADATA'
110-
with metadata.open('w', encoding='utf-8') as strm:
111-
strm.write('Version: 1.0\n')
112-
return 'CherryPy'
105+
fixtures.build_files(self.make_pkg('my_pkg'), self.site_dir)
106+
assert version('my-pkg') == '1.0'
113107

114108
def test_dist_name_found_as_any_case(self):
115109
"""
116110
Ensure the metadata loads when queried with any case.
117111
"""
118-
pkg_name = self.pkg_with_mixed_case(self.site_dir)
112+
pkg_name = 'CherryPy'
113+
fixtures.build_files(self.make_pkg(pkg_name), self.site_dir)
119114
assert version(pkg_name) == '1.0'
120115
assert version(pkg_name.lower()) == '1.0'
121116
assert version(pkg_name.upper()) == '1.0'
122117

118+
def test_unique_distributions(self):
119+
"""
120+
Two distributions varying only by non-normalized name on
121+
the file system should resolve as the same.
122+
"""
123+
fixtures.build_files(self.make_pkg('abc'), self.site_dir)
124+
before = list(_unique(distributions()))
125+
126+
alt_site_dir = self.fixtures.enter_context(fixtures.tempdir())
127+
self.fixtures.enter_context(self.add_sys_path(alt_site_dir))
128+
fixtures.build_files(self.make_pkg('ABC'), alt_site_dir)
129+
after = list(_unique(distributions()))
130+
131+
assert len(after) == len(before)
132+
123133

124134
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
125135
@staticmethod
@@ -128,11 +138,12 @@ def pkg_with_non_ascii_description(site_dir):
128138
Create minimal metadata for a package with non-ASCII in
129139
the description.
130140
"""
131-
metadata_dir = site_dir / 'portend.dist-info'
132-
metadata_dir.mkdir()
133-
metadata = metadata_dir / 'METADATA'
134-
with metadata.open('w', encoding='utf-8') as fp:
135-
fp.write('Description: pôrˈtend')
141+
contents = {
142+
'portend.dist-info': {
143+
'METADATA': 'Description: pôrˈtend',
144+
},
145+
}
146+
fixtures.build_files(contents, site_dir)
136147
return 'portend'
137148

138149
@staticmethod
@@ -141,19 +152,15 @@ def pkg_with_non_ascii_description_egg_info(site_dir):
141152
Create minimal metadata for an egg-info package with
142153
non-ASCII in the description.
143154
"""
144-
metadata_dir = site_dir / 'portend.dist-info'
145-
metadata_dir.mkdir()
146-
metadata = metadata_dir / 'METADATA'
147-
with metadata.open('w', encoding='utf-8') as fp:
148-
fp.write(
149-
textwrap.dedent(
150-
"""
155+
contents = {
156+
'portend.dist-info': {
157+
'METADATA': """
151158
Name: portend
152159
153-
pôrˈtend
154-
"""
155-
).strip()
156-
)
160+
pôrˈtend""",
161+
},
162+
}
163+
fixtures.build_files(contents, site_dir)
157164
return 'portend'
158165

159166
def test_metadata_loads(self):

Lib/test/test_importlib/test_metadata_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,15 @@ def test_entry_points_distribution(self):
8989
self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg'))
9090
self.assertEqual(ep.dist.version, "1.0.0")
9191

92-
def test_entry_points_unique_packages(self):
92+
def test_entry_points_unique_packages_normalized(self):
9393
"""
9494
Entry points should only be exposed for the first package
95-
on sys.path with a given name.
95+
on sys.path with a given name (even when normalized).
9696
"""
9797
alt_site_dir = self.fixtures.enter_context(fixtures.tempdir())
9898
self.fixtures.enter_context(self.add_sys_path(alt_site_dir))
9999
alt_pkg = {
100-
"distinfo_pkg-1.1.0.dist-info": {
100+
"DistInfo_pkg-1.1.0.dist-info": {
101101
"METADATA": """
102102
Name: distinfo-pkg
103103
Version: 1.1.0
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Now raise ``ValueError`` when ``None`` or an empty string are passed to
2+
``Distribution.from_name`` (and other callers).

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy