Skip to content

Commit

Permalink
Sync with importlib_metadata 6.4.1
Browse files Browse the repository at this point in the history
  • Loading branch information
jaraco committed Apr 16, 2023
1 parent a210cac commit 994c91e
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 72 deletions.
4 changes: 4 additions & 0 deletions Doc/library/importlib.metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ Python module or `Import Package <https://packaging.python.org/en/latest/glossar
>>> packages_distributions()
{'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...}

Some editable installs, `do not supply top-level names
<https://github.com/pypa/packaging-problems/issues/609>`_, and thus this
function is not reliable with such installs.

.. versionadded:: 3.10

.. _distributions:
Expand Down
79 changes: 65 additions & 14 deletions Lib/importlib/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import functools
import itertools
import posixpath
import contextlib
import collections
import inspect

from . import _adapters, _meta
from ._collections import FreezableDefaultDict, Pair
Expand All @@ -24,7 +26,7 @@
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__ = [
Expand Down Expand Up @@ -341,11 +343,11 @@ def __repr__(self):
return f'<FileHash mode: {self.mode} value: {self.value}>'


class Distribution:
class Distribution(metaclass=abc.ABCMeta):
"""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.
Expand Down Expand Up @@ -419,14 +421,15 @@ def metadata(self) -> _meta.PackageMetadata:
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
# effect is to just end up using the PathDistribution's self._path
# (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
Expand Down Expand Up @@ -455,8 +458,8 @@ def files(self):
: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.
"""

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

@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):
"""
Expand All @@ -480,10 +493,43 @@ def _read_files_distinfo(self):
text = self.read_text('RECORD')
return text and text.splitlines()

def _read_files_egginfo(self):
def _read_files_egginfo_installed(self):
"""
SOURCES.txt might contain literal commas, so wrap each line
in quotes.
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.
"""
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())
Expand Down Expand Up @@ -886,8 +932,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)
21 changes: 21 additions & 0 deletions Lib/importlib/metadata/_adapters.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import functools
import warnings
import re
import textwrap
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(
Expand Down Expand Up @@ -39,6 +50,16 @@ def __init__(self, *args, **kwargs):
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"
Expand Down
28 changes: 22 additions & 6 deletions Lib/importlib/metadata/_meta.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -17,7 +18,21 @@ def __getitem__(self, key: str) -> str:
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.
"""
Expand All @@ -29,18 +44,19 @@ def json(self) -> Dict[str, Union[str, List[str]]]:
"""


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:
Expand Down
Loading

0 comments on commit 994c91e

Please sign in to comment.