gh-117089: Apply changes from importlib_metadata 7.1.0 (#117094)

* Apply changes from importlib_metadata 7.1.0

* Include the data sources in the makefile (even though they're not needed)
This commit is contained in:
Jason R. Coombs 2024-03-20 23:01:24 -04:00 committed by GitHub
parent f4cc77d494
commit 667294d5b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 212 additions and 95 deletions

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import os
import re
import abc
@ -26,7 +28,7 @@ from contextlib import suppress
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
from typing import Iterable, List, Mapping, Optional, Set, Union, cast
from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast
__all__ = [
'Distribution',
@ -163,17 +165,17 @@ class EntryPoint:
value: str
group: str
dist: Optional['Distribution'] = None
dist: Optional[Distribution] = None
def __init__(self, name: str, value: str, group: str) -> None:
vars(self).update(name=name, value=value, group=group)
def load(self):
def load(self) -> Any:
"""Load the entry point from its definition. If only a module
is indicated by the value, return that module. Otherwise,
return the named object.
"""
match = self.pattern.match(self.value)
match = cast(Match, self.pattern.match(self.value))
module = import_module(match.group('module'))
attrs = filter(None, (match.group('attr') or '').split('.'))
return functools.reduce(getattr, attrs, module)
@ -268,7 +270,7 @@ class EntryPoints(tuple):
"""
return '%s(%r)' % (self.__class__.__name__, tuple(self))
def select(self, **params):
def select(self, **params) -> EntryPoints:
"""
Select entry points from self that match the
given parameters (typically group and/or name).
@ -304,19 +306,17 @@ class EntryPoints(tuple):
class PackagePath(pathlib.PurePosixPath):
"""A reference to a path in a package"""
hash: Optional["FileHash"]
hash: Optional[FileHash]
size: int
dist: "Distribution"
dist: Distribution
def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
with self.locate().open(encoding=encoding) as stream:
return stream.read()
return self.locate().read_text(encoding=encoding)
def read_binary(self) -> bytes:
with self.locate().open('rb') as stream:
return stream.read()
return self.locate().read_bytes()
def locate(self) -> pathlib.Path:
def locate(self) -> SimplePath:
"""Return a path-like object for this path"""
return self.dist.locate_file(self)
@ -330,6 +330,7 @@ class FileHash:
class DeprecatedNonAbstract:
# Required until Python 3.14
def __new__(cls, *args, **kwargs):
all_names = {
name for subclass in inspect.getmro(cls) for name in vars(subclass)
@ -349,25 +350,48 @@ class DeprecatedNonAbstract:
class Distribution(DeprecatedNonAbstract):
"""A Python distribution package."""
"""
An abstract Python distribution package.
Custom providers may derive from this class and define
the abstract methods to provide a concrete implementation
for their environment. Some providers may opt to override
the default implementation of some properties to bypass
the file-reading mechanism.
"""
@abc.abstractmethod
def read_text(self, filename) -> Optional[str]:
"""Attempt to load metadata file given by the name.
Python distribution metadata is organized by blobs of text
typically represented as "files" in the metadata directory
(e.g. package-1.0.dist-info). These files include things
like:
- METADATA: The distribution metadata including fields
like Name and Version and Description.
- entry_points.txt: A series of entry points as defined in
`the entry points spec <https://packaging.python.org/en/latest/specifications/entry-points/#file-format>`_.
- RECORD: A record of files according to
`this recording spec <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-record-file>`_.
A package may provide any set of files, including those
not listed here or none at all.
:param filename: The name of the file in the distribution info.
:return: The text if found, otherwise None.
"""
@abc.abstractmethod
def locate_file(self, path: Union[str, os.PathLike[str]]) -> pathlib.Path:
def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
"""
Given a path to a file in this distribution, return a path
Given a path to a file in this distribution, return a SimplePath
to it.
"""
@classmethod
def from_name(cls, name: str) -> "Distribution":
def from_name(cls, name: str) -> Distribution:
"""Return the Distribution for the given package name.
:param name: The name of the distribution package to search for.
@ -385,16 +409,18 @@ class Distribution(DeprecatedNonAbstract):
raise PackageNotFoundError(name)
@classmethod
def discover(cls, **kwargs) -> Iterable["Distribution"]:
def discover(
cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
) -> Iterable[Distribution]:
"""Return an iterable of Distribution objects for all packages.
Pass a ``context`` or pass keyword arguments for constructing
a context.
:context: A ``DistributionFinder.Context`` object.
:return: Iterable of Distribution objects for all packages.
:return: Iterable of Distribution objects for packages matching
the context.
"""
context = kwargs.pop('context', None)
if context and kwargs:
raise ValueError("cannot accept context and kwargs")
context = context or DistributionFinder.Context(**kwargs)
@ -403,8 +429,8 @@ class Distribution(DeprecatedNonAbstract):
)
@staticmethod
def at(path: Union[str, os.PathLike[str]]) -> "Distribution":
"""Return a Distribution for the indicated metadata path
def at(path: str | os.PathLike[str]) -> Distribution:
"""Return a Distribution for the indicated metadata path.
:param path: a string or path-like object
:return: a concrete Distribution instance for the path
@ -413,7 +439,7 @@ class Distribution(DeprecatedNonAbstract):
@staticmethod
def _discover_resolvers():
"""Search the meta_path for resolvers."""
"""Search the meta_path for resolvers (MetadataPathFinders)."""
declared = (
getattr(finder, 'find_distributions', None) for finder in sys.meta_path
)
@ -424,7 +450,11 @@ class Distribution(DeprecatedNonAbstract):
"""Return the parsed metadata for this Distribution.
The returned object will have keys that name the various bits of
metadata. See PEP 566 for details.
metadata per the
`Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_.
Custom providers may provide the METADATA file or override this
property.
"""
opt_text = (
self.read_text('METADATA')
@ -454,6 +484,12 @@ class Distribution(DeprecatedNonAbstract):
@property
def entry_points(self) -> EntryPoints:
"""
Return EntryPoints for this distribution.
Custom providers may provide the ``entry_points.txt`` file
or override this property.
"""
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
@property
@ -466,6 +502,10 @@ class Distribution(DeprecatedNonAbstract):
(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.
Custom providers are recommended to provide a "RECORD" file (in
``read_text``) or override this property to allow for callers to be
able to resolve filenames provided by the package.
"""
def make_file(name, hash=None, size_str=None):
@ -497,7 +537,7 @@ class Distribution(DeprecatedNonAbstract):
def _read_files_distinfo(self):
"""
Read the lines of RECORD
Read the lines of RECORD.
"""
text = self.read_text('RECORD')
return text and text.splitlines()
@ -611,6 +651,9 @@ class Distribution(DeprecatedNonAbstract):
class DistributionFinder(MetaPathFinder):
"""
A MetaPathFinder capable of discovering installed distributions.
Custom providers should implement this interface in order to
supply metadata.
"""
class Context:
@ -623,6 +666,17 @@ class DistributionFinder(MetaPathFinder):
Each DistributionFinder may expect any parameters
and should attempt to honor the canonical
parameters defined below when appropriate.
This mechanism gives a custom provider a means to
solicit additional details from the caller beyond
"name" and "path" when searching distributions.
For example, imagine a provider that exposes suites
of packages in either a "public" or "private" ``realm``.
A caller may wish to query only for distributions in
a particular realm and could call
``distributions(realm="private")`` to signal to the
custom provider to only include distributions from that
realm.
"""
name = None
@ -658,11 +712,18 @@ class DistributionFinder(MetaPathFinder):
class FastPath:
"""
Micro-optimized class for searching a path for
children.
Micro-optimized class for searching a root for children.
Root is a path on the file system that may contain metadata
directories either as natural directories or within a zip file.
>>> FastPath('').children()
['...']
FastPath objects are cached and recycled for any given root.
>>> FastPath('foobar') is FastPath('foobar')
True
"""
@functools.lru_cache() # type: ignore
@ -704,7 +765,19 @@ class FastPath:
class Lookup:
"""
A micro-optimized class for searching a (fast) path for metadata.
"""
def __init__(self, path: FastPath):
"""
Calculate all of the children representing metadata.
From the children in the path, calculate early all of the
children that appear to represent metadata (infos) or legacy
metadata (eggs).
"""
base = os.path.basename(path.root).lower()
base_is_egg = base.endswith(".egg")
self.infos = FreezableDefaultDict(list)
@ -725,7 +798,10 @@ class Lookup:
self.infos.freeze()
self.eggs.freeze()
def search(self, prepared):
def search(self, prepared: Prepared):
"""
Yield all infos and eggs matching the Prepared query.
"""
infos = (
self.infos[prepared.normalized]
if prepared
@ -741,13 +817,28 @@ class Lookup:
class Prepared:
"""
A prepared search for metadata on a possibly-named package.
A prepared search query for metadata on a possibly-named package.
Pre-calculates the normalization to prevent repeated operations.
>>> none = Prepared(None)
>>> none.normalized
>>> none.legacy_normalized
>>> bool(none)
False
>>> sample = Prepared('Sample__Pkg-name.foo')
>>> sample.normalized
'sample_pkg_name_foo'
>>> sample.legacy_normalized
'sample__pkg_name.foo'
>>> bool(sample)
True
"""
normalized = None
legacy_normalized = None
def __init__(self, name):
def __init__(self, name: Optional[str]):
self.name = name
if name is None:
return
@ -777,7 +868,7 @@ class MetadataPathFinder(DistributionFinder):
@classmethod
def find_distributions(
cls, context=DistributionFinder.Context()
) -> Iterable["PathDistribution"]:
) -> Iterable[PathDistribution]:
"""
Find distributions.
@ -810,7 +901,7 @@ class PathDistribution(Distribution):
"""
self._path = path
def read_text(self, filename: Union[str, os.PathLike[str]]) -> Optional[str]:
def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
with suppress(
FileNotFoundError,
IsADirectoryError,
@ -824,7 +915,7 @@ class PathDistribution(Distribution):
read_text.__doc__ = Distribution.read_text.__doc__
def locate_file(self, path: Union[str, os.PathLike[str]]) -> pathlib.Path:
def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
return self._path.parent / path
@property

View File

@ -1,3 +1,6 @@
from __future__ import annotations
import os
from typing import Protocol
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
@ -6,30 +9,27 @@ _T = TypeVar("_T")
class PackageMetadata(Protocol):
def __len__(self) -> int:
... # pragma: no cover
def __len__(self) -> int: ... # pragma: no cover
def __contains__(self, item: str) -> bool:
... # pragma: no cover
def __contains__(self, item: str) -> bool: ... # pragma: no cover
def __getitem__(self, key: str) -> str:
... # pragma: no cover
def __getitem__(self, key: str) -> str: ... # pragma: no cover
def __iter__(self) -> Iterator[str]:
... # pragma: no cover
def __iter__(self) -> Iterator[str]: ... # pragma: no cover
@overload
def get(self, name: str, failobj: None = None) -> Optional[str]:
... # pragma: no cover
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
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
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]:
@ -44,20 +44,24 @@ class PackageMetadata(Protocol):
"""
class SimplePath(Protocol[_T]):
class SimplePath(Protocol):
"""
A minimal subset of pathlib.Path required by PathDistribution.
A minimal subset of pathlib.Path required by Distribution.
"""
def joinpath(self, other: Union[str, _T]) -> _T:
... # pragma: no cover
def joinpath(
self, other: Union[str, os.PathLike[str]]
) -> SimplePath: ... # pragma: no cover
def __truediv__(self, other: Union[str, _T]) -> _T:
... # pragma: no cover
def __truediv__(
self, other: Union[str, os.PathLike[str]]
) -> SimplePath: ... # pragma: no cover
@property
def parent(self) -> _T:
... # pragma: no cover
def parent(self) -> SimplePath: ... # pragma: no cover
def read_text(self) -> str:
... # pragma: no cover
def read_text(self, encoding=None) -> str: ... # pragma: no cover
def read_bytes(self) -> bytes: ... # pragma: no cover
def exists(self) -> bool: ... # pragma: no cover

View File

@ -17,20 +17,15 @@ FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore
@runtime_checkable
class TreeMaker(Protocol):
def __truediv__(self, *args, **kwargs):
... # pragma: no cover
def __truediv__(self, *args, **kwargs): ... # pragma: no cover
def mkdir(self, **kwargs):
... # pragma: no cover
def mkdir(self, **kwargs): ... # pragma: no cover
def write_text(self, content, **kwargs):
... # pragma: no cover
def write_text(self, content, **kwargs): ... # pragma: no cover
def write_bytes(self, content):
... # pragma: no cover
def write_bytes(self, content): ... # pragma: no cover
def symlink_to(self, target):
... # pragma: no cover
def symlink_to(self, target): ... # pragma: no cover
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:

View File

@ -0,0 +1,2 @@
def main():
return 'example'

View File

@ -0,0 +1,11 @@
from setuptools import setup
setup(
name='example',
version='21.12',
license='Apache Software License',
packages=['example'],
entry_points={
'console_scripts': ['example = example:main', 'Example=example:main'],
},
)

View File

@ -0,0 +1,2 @@
def main():
return "example"

View File

@ -0,0 +1,10 @@
[build-system]
build-backend = 'trampolim'
requires = ['trampolim']
[project]
name = 'example2'
version = '1.0.0'
[project.scripts]
example = 'example2:main'

View File

@ -10,7 +10,7 @@ import functools
import contextlib
from test.support import import_helper
from test.support.os_helper import FS_NONASCII
from test.support import os_helper
from test.support import requires_zlib
from . import _path
@ -143,15 +143,13 @@ class DistInfoPkgEditable(DistInfoPkg):
some_hash = '524127ce937f7cb65665130c695abd18ca386f60bb29687efb976faa1596fdcc'
files: FilesSpec = {
'distinfo_pkg-1.0.0.dist-info': {
'direct_url.json': json.dumps(
{
'direct_url.json': json.dumps({
"archive_info": {
"hash": f"sha256={some_hash}",
"hashes": {"sha256": f"{some_hash}"},
},
"url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl",
}
)
})
},
}
@ -340,7 +338,9 @@ def record_names(file_defs):
class FileBuilder:
def unicode_filename(self):
return FS_NONASCII or self.skip("File system does not support non-ascii.")
return os_helper.FS_NONASCII or self.skip(
"File system does not support non-ascii."
)
def DALS(str):

View File

@ -2,6 +2,7 @@ import re
import pickle
import unittest
import warnings
import importlib
import importlib.metadata
import contextlib
from test.support import os_helper
@ -308,12 +309,10 @@ class TestEntryPoints(unittest.TestCase):
"""
EntryPoint objects are sortable, but result is undefined.
"""
sorted(
[
sorted([
EntryPoint(name='b', value='val', group='group'),
EntryPoint(name='a', value='val', group='group'),
]
)
])
class FileSystem(
@ -380,8 +379,7 @@ class PackagesDistributionsTest(
'all_distributions-1.0.0.dist-info': metadata,
}
for i, suffix in enumerate(suffixes):
files.update(
{
files.update({
f'importable-name {i}{suffix}': '',
f'in_namespace_{i}': {
f'mod{suffix}': '',
@ -390,8 +388,7 @@ class PackagesDistributionsTest(
'__init__.py': '',
f'mod{suffix}': '',
},
}
)
})
metadata.update(RECORD=fixtures.build_record(files))
fixtures.build_files(files, prefix=self.site_dir)

View File

@ -2356,6 +2356,11 @@ TESTSUBDIRS= idlelib/idle_test \
test/test_importlib/import_ \
test/test_importlib/metadata \
test/test_importlib/metadata/data \
test/test_importlib/metadata/data/sources \
test/test_importlib/metadata/data/sources/example \
test/test_importlib/metadata/data/sources/example/example \
test/test_importlib/metadata/data/sources/example2 \
test/test_importlib/metadata/data/sources/example2/example2 \
test/test_importlib/namespace_pkgs \
test/test_importlib/namespace_pkgs/both_portions \
test/test_importlib/namespace_pkgs/both_portions/foo \