summaryrefslogtreecommitdiffstats
path: root/Lib/importlib/metadata
diff options
context:
space:
mode:
authorJason R. Coombs <jaraco@jaraco.com>2023-04-21 02:12:48 (GMT)
committerGitHub <noreply@github.com>2023-04-21 02:12:48 (GMT)
commit3e0fec7e07a71bdeeab7554e980110fbc47763b9 (patch)
treec95fc47a9ee48d5606f4eb6cc45703f7be568769 /Lib/importlib/metadata
parent5c00a6224d55f8818ef567b93f484b5429e2ae80 (diff)
downloadcpython-3e0fec7e07a71bdeeab7554e980110fbc47763b9.zip
cpython-3e0fec7e07a71bdeeab7554e980110fbc47763b9.tar.gz
cpython-3e0fec7e07a71bdeeab7554e980110fbc47763b9.tar.bz2
Sync with importlib_metadata 6.5 (GH-103584)
Diffstat (limited to 'Lib/importlib/metadata')
-rw-r--r--Lib/importlib/metadata/__init__.py98
-rw-r--r--Lib/importlib/metadata/_adapters.py21
-rw-r--r--Lib/importlib/metadata/_meta.py28
3 files changed, 127 insertions, 20 deletions
diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py
index 40ab1a1..b8eb19d 100644
--- a/Lib/importlib/metadata/__init__.py
+++ b/Lib/importlib/metadata/__init__.py
@@ -12,7 +12,9 @@ import warnings
import functools
import itertools
import posixpath
+import contextlib
import collections
+import inspect
from . import _adapters, _meta
from ._collections import FreezableDefaultDict, Pair
@@ -24,7 +26,7 @@ from contextlib import suppress
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
-from typing import List, Mapping, Optional
+from typing import List, Mapping, Optional, cast
__all__ = [
@@ -341,11 +343,30 @@ class FileHash:
return f'<FileHash mode: {self.mode} value: {self.value}>'
-class Distribution:
+class DeprecatedNonAbstract:
+ def __new__(cls, *args, **kwargs):
+ all_names = {
+ name for subclass in inspect.getmro(cls) for name in vars(subclass)
+ }
+ abstract = {
+ name
+ for name in all_names
+ if getattr(getattr(cls, name), '__isabstractmethod__', False)
+ }
+ if abstract:
+ warnings.warn(
+ f"Unimplemented abstract methods {abstract}",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return super().__new__(cls)
+
+
+class Distribution(DeprecatedNonAbstract):
"""A Python distribution package."""
@abc.abstractmethod
- def read_text(self, filename):
+ def read_text(self, filename) -> Optional[str]:
"""Attempt to load metadata file given by the name.
:param filename: The name of the file in the distribution info.
@@ -419,7 +440,7 @@ class Distribution:
The returned object will have keys that name the various bits of
metadata. See PEP 566 for details.
"""
- text = (
+ opt_text = (
self.read_text('METADATA')
or self.read_text('PKG-INFO')
# This last clause is here to support old egg-info files. Its
@@ -427,6 +448,7 @@ class Distribution:
# (which points to the egg-info file) attribute unchanged.
or self.read_text('')
)
+ text = cast(str, opt_text)
return _adapters.Message(email.message_from_string(text))
@property
@@ -455,8 +477,8 @@ class Distribution:
:return: List of PackagePath for this distribution or None
Result is `None` if the metadata file that enumerates files
- (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
- missing.
+ (i.e. RECORD for dist-info, or installed-files.txt or
+ SOURCES.txt for egg-info) is missing.
Result may be empty if the metadata exists but is empty.
"""
@@ -469,9 +491,19 @@ class Distribution:
@pass_none
def make_files(lines):
- return list(starmap(make_file, csv.reader(lines)))
+ return starmap(make_file, csv.reader(lines))
- return make_files(self._read_files_distinfo() or self._read_files_egginfo())
+ @pass_none
+ def skip_missing_files(package_paths):
+ return list(filter(lambda path: path.locate().exists(), package_paths))
+
+ return skip_missing_files(
+ make_files(
+ self._read_files_distinfo()
+ or self._read_files_egginfo_installed()
+ or self._read_files_egginfo_sources()
+ )
+ )
def _read_files_distinfo(self):
"""
@@ -480,10 +512,43 @@ class Distribution:
text = self.read_text('RECORD')
return text and text.splitlines()
- def _read_files_egginfo(self):
+ def _read_files_egginfo_installed(self):
+ """
+ Read installed-files.txt and return lines in a similar
+ CSV-parsable format as RECORD: each file must be placed
+ relative to the site-packages directory, and must also be
+ quoted (since file names can contain literal commas).
+
+ This file is written when the package is installed by pip,
+ but it might not be written for other installation methods.
+ Hence, even if we can assume that this file is accurate
+ when it exists, we cannot assume that it always exists.
"""
- SOURCES.txt might contain literal commas, so wrap each line
- in quotes.
+ text = self.read_text('installed-files.txt')
+ # We need to prepend the .egg-info/ subdir to the lines in this file.
+ # But this subdir is only available in the PathDistribution's self._path
+ # which is not easily accessible from this base class...
+ subdir = getattr(self, '_path', None)
+ if not text or not subdir:
+ return
+ with contextlib.suppress(Exception):
+ ret = [
+ str((subdir / line).resolve().relative_to(self.locate_file('')))
+ for line in text.splitlines()
+ ]
+ return map('"{}"'.format, ret)
+
+ def _read_files_egginfo_sources(self):
+ """
+ Read SOURCES.txt and return lines in a similar CSV-parsable
+ format as RECORD: each file name must be quoted (since it
+ might contain literal commas).
+
+ Note that SOURCES.txt is not a reliable source for what
+ files are installed by a package. This file is generated
+ for a source archive, and the files that are present
+ there (e.g. setup.py) may not correctly reflect the files
+ that are present after the package has been installed.
"""
text = self.read_text('SOURCES.txt')
return text and map('"{}"'.format, text.splitlines())
@@ -886,8 +951,13 @@ def _top_level_declared(dist):
def _top_level_inferred(dist):
- return {
- f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
+ opt_names = {
+ f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
for f in always_iterable(dist.files)
- if f.suffix == ".py"
}
+
+ @pass_none
+ def importable_name(name):
+ return '.' not in name
+
+ return filter(importable_name, opt_names)
diff --git a/Lib/importlib/metadata/_adapters.py b/Lib/importlib/metadata/_adapters.py
index aa460d3..6aed69a 100644
--- a/Lib/importlib/metadata/_adapters.py
+++ b/Lib/importlib/metadata/_adapters.py
@@ -1,3 +1,5 @@
+import functools
+import warnings
import re
import textwrap
import email.message
@@ -5,6 +7,15 @@ import email.message
from ._text import FoldedCase
+# Do not remove prior to 2024-01-01 or Python 3.14
+_warn = functools.partial(
+ warnings.warn,
+ "Implicit None on return values is deprecated and will raise KeyErrors.",
+ DeprecationWarning,
+ stacklevel=2,
+)
+
+
class Message(email.message.Message):
multiple_use_keys = set(
map(
@@ -39,6 +50,16 @@ class Message(email.message.Message):
def __iter__(self):
return super().__iter__()
+ def __getitem__(self, item):
+ """
+ Warn users that a ``KeyError`` can be expected when a
+ mising key is supplied. Ref python/importlib_metadata#371.
+ """
+ res = super().__getitem__(item)
+ if res is None:
+ _warn()
+ return res
+
def _repair_headers(self):
def redent(value):
"Correct for RFC822 indentation"
diff --git a/Lib/importlib/metadata/_meta.py b/Lib/importlib/metadata/_meta.py
index d5c0576..c9a7ef9 100644
--- a/Lib/importlib/metadata/_meta.py
+++ b/Lib/importlib/metadata/_meta.py
@@ -1,4 +1,5 @@
-from typing import Any, Dict, Iterator, List, Protocol, TypeVar, Union
+from typing import Protocol
+from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
_T = TypeVar("_T")
@@ -17,7 +18,21 @@ class PackageMetadata(Protocol):
def __iter__(self) -> Iterator[str]:
... # pragma: no cover
- def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
+ @overload
+ def get(self, name: str, failobj: None = None) -> Optional[str]:
+ ... # pragma: no cover
+
+ @overload
+ def get(self, name: str, failobj: _T) -> Union[str, _T]:
+ ... # pragma: no cover
+
+ # overload per python/importlib_metadata#435
+ @overload
+ def get_all(self, name: str, failobj: None = None) -> Optional[List[Any]]:
+ ... # pragma: no cover
+
+ @overload
+ def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
"""
Return all values associated with a possibly multi-valued key.
"""
@@ -29,18 +44,19 @@ class PackageMetadata(Protocol):
"""
-class SimplePath(Protocol):
+class SimplePath(Protocol[_T]):
"""
A minimal subset of pathlib.Path required by PathDistribution.
"""
- def joinpath(self) -> 'SimplePath':
+ def joinpath(self) -> _T:
... # pragma: no cover
- def __truediv__(self) -> 'SimplePath':
+ def __truediv__(self, other: Union[str, _T]) -> _T:
... # pragma: no cover
- def parent(self) -> 'SimplePath':
+ @property
+ def parent(self) -> _T:
... # pragma: no cover
def read_text(self) -> str: