Skip to content

Commit 994c91e

Browse files
committed
Sync with importlib_metadata 6.4.1
1 parent a210cac commit 994c91e

File tree

7 files changed

+365
-72
lines changed

7 files changed

+365
-72
lines changed

Doc/library/importlib.metadata.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,10 @@ Python module or `Import Package <https://packaging.python.org/en/latest/glossar
308308
>>> packages_distributions()
309309
{'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...}
310310

311+
Some editable installs, `do not supply top-level names
312+
<https://github.com/pypa/packaging-problems/issues/609>`_, and thus this
313+
function is not reliable with such installs.
314+
311315
.. versionadded:: 3.10
312316

313317
.. _distributions:

Lib/importlib/metadata/__init__.py

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import functools
1313
import itertools
1414
import posixpath
15+
import contextlib
1516
import collections
17+
import inspect
1618

1719
from . import _adapters, _meta
1820
from ._collections import FreezableDefaultDict, Pair
@@ -24,7 +26,7 @@
2426
from importlib import import_module
2527
from importlib.abc import MetaPathFinder
2628
from itertools import starmap
27-
from typing import List, Mapping, Optional
29+
from typing import List, Mapping, Optional, cast
2830

2931

3032
__all__ = [
@@ -341,11 +343,11 @@ def __repr__(self):
341343
return f'<FileHash mode: {self.mode} value: {self.value}>'
342344

343345

344-
class Distribution:
346+
class Distribution(metaclass=abc.ABCMeta):
345347
"""A Python distribution package."""
346348

347349
@abc.abstractmethod
348-
def read_text(self, filename):
350+
def read_text(self, filename) -> Optional[str]:
349351
"""Attempt to load metadata file given by the name.
350352
351353
:param filename: The name of the file in the distribution info.
@@ -419,14 +421,15 @@ def metadata(self) -> _meta.PackageMetadata:
419421
The returned object will have keys that name the various bits of
420422
metadata. See PEP 566 for details.
421423
"""
422-
text = (
424+
opt_text = (
423425
self.read_text('METADATA')
424426
or self.read_text('PKG-INFO')
425427
# This last clause is here to support old egg-info files. Its
426428
# effect is to just end up using the PathDistribution's self._path
427429
# (which points to the egg-info file) attribute unchanged.
428430
or self.read_text('')
429431
)
432+
text = cast(str, opt_text)
430433
return _adapters.Message(email.message_from_string(text))
431434

432435
@property
@@ -455,8 +458,8 @@ def files(self):
455458
:return: List of PackagePath for this distribution or None
456459
457460
Result is `None` if the metadata file that enumerates files
458-
(i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
459-
missing.
461+
(i.e. RECORD for dist-info, or installed-files.txt or
462+
SOURCES.txt for egg-info) is missing.
460463
Result may be empty if the metadata exists but is empty.
461464
"""
462465

@@ -469,9 +472,19 @@ def make_file(name, hash=None, size_str=None):
469472

470473
@pass_none
471474
def make_files(lines):
472-
return list(starmap(make_file, csv.reader(lines)))
475+
return starmap(make_file, csv.reader(lines))
473476

474-
return make_files(self._read_files_distinfo() or self._read_files_egginfo())
477+
@pass_none
478+
def skip_missing_files(package_paths):
479+
return list(filter(lambda path: path.locate().exists(), package_paths))
480+
481+
return skip_missing_files(
482+
make_files(
483+
self._read_files_distinfo()
484+
or self._read_files_egginfo_installed()
485+
or self._read_files_egginfo_sources()
486+
)
487+
)
475488

476489
def _read_files_distinfo(self):
477490
"""
@@ -480,10 +493,43 @@ def _read_files_distinfo(self):
480493
text = self.read_text('RECORD')
481494
return text and text.splitlines()
482495

483-
def _read_files_egginfo(self):
496+
def _read_files_egginfo_installed(self):
484497
"""
485-
SOURCES.txt might contain literal commas, so wrap each line
486-
in quotes.
498+
Read installed-files.txt and return lines in a similar
499+
CSV-parsable format as RECORD: each file must be placed
500+
relative to the site-packages directory, and must also be
501+
quoted (since file names can contain literal commas).
502+
503+
This file is written when the package is installed by pip,
504+
but it might not be written for other installation methods.
505+
Hence, even if we can assume that this file is accurate
506+
when it exists, we cannot assume that it always exists.
507+
"""
508+
text = self.read_text('installed-files.txt')
509+
# We need to prepend the .egg-info/ subdir to the lines in this file.
510+
# But this subdir is only available in the PathDistribution's self._path
511+
# which is not easily accessible from this base class...
512+
subdir = getattr(self, '_path', None)
513+
if not text or not subdir:
514+
return
515+
with contextlib.suppress(Exception):
516+
ret = [
517+
str((subdir / line).resolve().relative_to(self.locate_file('')))
518+
for line in text.splitlines()
519+
]
520+
return map('"{}"'.format, ret)
521+
522+
def _read_files_egginfo_sources(self):
523+
"""
524+
Read SOURCES.txt and return lines in a similar CSV-parsable
525+
format as RECORD: each file name must be quoted (since it
526+
might contain literal commas).
527+
528+
Note that SOURCES.txt is not a reliable source for what
529+
files are installed by a package. This file is generated
530+
for a source archive, and the files that are present
531+
there (e.g. setup.py) may not correctly reflect the files
532+
that are present after the package has been installed.
487533
"""
488534
text = self.read_text('SOURCES.txt')
489535
return text and map('"{}"'.format, text.splitlines())
@@ -886,8 +932,13 @@ def _top_level_declared(dist):
886932

887933

888934
def _top_level_inferred(dist):
889-
return {
890-
f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
935+
opt_names = {
936+
f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
891937
for f in always_iterable(dist.files)
892-
if f.suffix == ".py"
893938
}
939+
940+
@pass_none
941+
def importable_name(name):
942+
return '.' not in name
943+
944+
return filter(importable_name, opt_names)

Lib/importlib/metadata/_adapters.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1+
import functools
2+
import warnings
13
import re
24
import textwrap
35
import email.message
46

57
from ._text import FoldedCase
68

79

10+
# Do not remove prior to 2024-01-01 or Python 3.14
11+
_warn = functools.partial(
12+
warnings.warn,
13+
"Implicit None on return values is deprecated and will raise KeyErrors.",
14+
DeprecationWarning,
15+
stacklevel=2,
16+
)
17+
18+
819
class Message(email.message.Message):
920
multiple_use_keys = set(
1021
map(
@@ -39,6 +50,16 @@ def __init__(self, *args, **kwargs):
3950
def __iter__(self):
4051
return super().__iter__()
4152

53+
def __getitem__(self, item):
54+
"""
55+
Warn users that a ``KeyError`` can be expected when a
56+
mising key is supplied. Ref python/importlib_metadata#371.
57+
"""
58+
res = super().__getitem__(item)
59+
if res is None:
60+
_warn()
61+
return res
62+
4263
def _repair_headers(self):
4364
def redent(value):
4465
"Correct for RFC822 indentation"

Lib/importlib/metadata/_meta.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Any, Dict, Iterator, List, Protocol, TypeVar, Union
1+
from typing import Protocol
2+
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
23

34

45
_T = TypeVar("_T")
@@ -17,7 +18,21 @@ def __getitem__(self, key: str) -> str:
1718
def __iter__(self) -> Iterator[str]:
1819
... # pragma: no cover
1920

20-
def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
21+
@overload
22+
def get(self, name: str, failobj: None = None) -> Optional[str]:
23+
... # pragma: no cover
24+
25+
@overload
26+
def get(self, name: str, failobj: _T) -> Union[str, _T]:
27+
... # pragma: no cover
28+
29+
# overload per python/importlib_metadata#435
30+
@overload
31+
def get_all(self, name: str, failobj: None = None) -> Optional[List[Any]]:
32+
... # pragma: no cover
33+
34+
@overload
35+
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
2136
"""
2237
Return all values associated with a possibly multi-valued key.
2338
"""
@@ -29,18 +44,19 @@ def json(self) -> Dict[str, Union[str, List[str]]]:
2944
"""
3045

3146

32-
class SimplePath(Protocol):
47+
class SimplePath(Protocol[_T]):
3348
"""
3449
A minimal subset of pathlib.Path required by PathDistribution.
3550
"""
3651

37-
def joinpath(self) -> 'SimplePath':
52+
def joinpath(self) -> _T:
3853
... # pragma: no cover
3954

40-
def __truediv__(self) -> 'SimplePath':
55+
def __truediv__(self, other: Union[str, _T]) -> _T:
4156
... # pragma: no cover
4257

43-
def parent(self) -> 'SimplePath':
58+
@property
59+
def parent(self) -> _T:
4460
... # pragma: no cover
4561

4662
def read_text(self) -> str:

0 commit comments

Comments
 (0)