]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-117089: Apply changes from importlib_metadata 7.1.0 (#117094)
authorJason R. Coombs <jaraco@jaraco.com>
Thu, 21 Mar 2024 03:01:24 +0000 (23:01 -0400)
committerGitHub <noreply@github.com>
Thu, 21 Mar 2024 03:01:24 +0000 (23:01 -0400)
* Apply changes from importlib_metadata 7.1.0

* Include the data sources in the makefile (even though they're not needed)

Lib/importlib/metadata/__init__.py
Lib/importlib/metadata/_meta.py
Lib/test/test_importlib/metadata/_path.py
Lib/test/test_importlib/metadata/data/sources/example/example/__init__.py [new file with mode: 0644]
Lib/test/test_importlib/metadata/data/sources/example/setup.py [new file with mode: 0644]
Lib/test/test_importlib/metadata/data/sources/example2/example2/__init__.py [new file with mode: 0644]
Lib/test/test_importlib/metadata/data/sources/example2/pyproject.toml [new file with mode: 0644]
Lib/test/test_importlib/metadata/fixtures.py
Lib/test/test_importlib/metadata/test_main.py
Makefile.pre.in

index 41c2a4a6088b5d7588ba1b95cd53a29dd75742bf..c8e59cac44194d621af65649cf54b9794ca81600 100644 (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
index f670016de7fef207636726e51c2754846a157ca8..1927d0f624d82f2fa12f81c80cce91279f039e84 100644 (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, encoding=None) -> str: ...  # pragma: no cover
+
+    def read_bytes(self) -> bytes: ...  # pragma: no cover
 
-    def read_text(self) -> str:
-        ...  # pragma: no cover
+    def exists(self) -> bool: ...  # pragma: no cover
index 25c799fa44cd55676dcb866e26dfbefa8a677e94..b3cfb9cd549d6cee133e211b0d3d9df188372ef1 100644 (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:
diff --git a/Lib/test/test_importlib/metadata/data/sources/example/example/__init__.py b/Lib/test/test_importlib/metadata/data/sources/example/example/__init__.py
new file mode 100644 (file)
index 0000000..ba73b74
--- /dev/null
@@ -0,0 +1,2 @@
+def main():
+    return 'example'
diff --git a/Lib/test/test_importlib/metadata/data/sources/example/setup.py b/Lib/test/test_importlib/metadata/data/sources/example/setup.py
new file mode 100644 (file)
index 0000000..479488a
--- /dev/null
@@ -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'],
+    },
+)
diff --git a/Lib/test/test_importlib/metadata/data/sources/example2/example2/__init__.py b/Lib/test/test_importlib/metadata/data/sources/example2/example2/__init__.py
new file mode 100644 (file)
index 0000000..de645c2
--- /dev/null
@@ -0,0 +1,2 @@
+def main():
+    return "example"
diff --git a/Lib/test/test_importlib/metadata/data/sources/example2/pyproject.toml b/Lib/test/test_importlib/metadata/data/sources/example2/pyproject.toml
new file mode 100644 (file)
index 0000000..011f475
--- /dev/null
@@ -0,0 +1,10 @@
+[build-system]
+build-backend = 'trampolim'
+requires = ['trampolim']
+
+[project]
+name = 'example2'
+version = '1.0.0'
+
+[project.scripts]
+example = 'example2:main'
index f23ac5d9019d1718de0083b52f70306f1ef7c94d..7ff94c9afe88e12648b7631da26fceb60bf45022 100644 (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(
-                {
-                    "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",
-                }
-            )
+            '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):
index 0a769b898412340a3e2ff17fb21d403b26b96474..c4accaeb9ba9ed367440ecf45e715a7670fa798b 100644 (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(
-            [
-                EntryPoint(name='b', value='val', group='group'),
-                EntryPoint(name='a', value='val', group='group'),
-            ]
-        )
+        sorted([
+            EntryPoint(name='b', value='val', group='group'),
+            EntryPoint(name='a', value='val', group='group'),
+        ])
 
 
 class FileSystem(
@@ -380,18 +379,16 @@ class PackagesDistributionsTest(
             'all_distributions-1.0.0.dist-info': metadata,
         }
         for i, suffix in enumerate(suffixes):
-            files.update(
-                {
-                    f'importable-name {i}{suffix}': '',
-                    f'in_namespace_{i}': {
-                        f'mod{suffix}': '',
-                    },
-                    f'in_package_{i}': {
-                        '__init__.py': '',
-                        f'mod{suffix}': '',
-                    },
-                }
-            )
+            files.update({
+                f'importable-name {i}{suffix}': '',
+                f'in_namespace_{i}': {
+                    f'mod{suffix}': '',
+                },
+                f'in_package_{i}': {
+                    '__init__.py': '',
+                    f'mod{suffix}': '',
+                },
+            })
         metadata.update(RECORD=fixtures.build_record(files))
         fixtures.build_files(files, prefix=self.site_dir)
 
index 1451cf3a31ff0214e76940b5dd8eb8a8e6cb07da..cacf14a52cb68e1dd2936bac034749224114378a 100644 (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 \