summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/importlib/metadata/__init__.py155
-rw-r--r--Lib/importlib/metadata/_meta.py52
-rw-r--r--Lib/test/test_importlib/metadata/_path.py15
-rw-r--r--Lib/test/test_importlib/metadata/data/sources/example/example/__init__.py2
-rw-r--r--Lib/test/test_importlib/metadata/data/sources/example/setup.py11
-rw-r--r--Lib/test/test_importlib/metadata/data/sources/example2/example2/__init__.py2
-rw-r--r--Lib/test/test_importlib/metadata/data/sources/example2/pyproject.toml10
-rw-r--r--Lib/test/test_importlib/metadata/fixtures.py22
-rw-r--r--Lib/test/test_importlib/metadata/test_main.py33
-rw-r--r--Makefile.pre.in5
10 files changed, 212 insertions, 95 deletions
diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py
index 41c2a4a6..c8e59ca 100644
--- a/Lib/importlib/metadata/__init__.py
+++ b/Lib/importlib/metadata/__init__.py
@@ -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
diff --git a/Lib/importlib/metadata/_meta.py b/Lib/importlib/metadata/_meta.py
index f670016..1927d0f 100644
--- a/Lib/importlib/metadata/_meta.py
+++ b/Lib/importlib/metadata/_meta.py
@@ -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
diff --git a/Lib/test/test_importlib/metadata/_path.py b/Lib/test/test_importlib/metadata/_path.py
index 25c799f..b3cfb9c 100644
--- a/Lib/test/test_importlib/metadata/_path.py
+++ b/Lib/test/test_importlib/metadata/_path.py
@@ -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
index 0000000..ba73b74
--- /dev/null
+++ b/Lib/test/test_importlib/metadata/data/sources/example/example/__init__.py
@@ -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
index 0000000..479488a
--- /dev/null
+++ b/Lib/test/test_importlib/metadata/data/sources/example/setup.py
@@ -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
index 0000000..de645c2
--- /dev/null
+++ b/Lib/test/test_importlib/metadata/data/sources/example2/example2/__init__.py
@@ -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
index 0000000..011f475
--- /dev/null
+++ b/Lib/test/test_importlib/metadata/data/sources/example2/pyproject.toml
@@ -0,0 +1,10 @@
+[build-system]
+build-backend = 'trampolim'
+requires = ['trampolim']
+
+[project]
+name = 'example2'
+version = '1.0.0'
+
+[project.scripts]
+example = 'example2:main'
diff --git a/Lib/test/test_importlib/metadata/fixtures.py b/Lib/test/test_importlib/metadata/fixtures.py
index f23ac5d..7ff94c9 100644
--- a/Lib/test/test_importlib/metadata/fixtures.py
+++ b/Lib/test/test_importlib/metadata/fixtures.py
@@ -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):
diff --git a/Lib/test/test_importlib/metadata/test_main.py b/Lib/test/test_importlib/metadata/test_main.py
index 0a769b8..c4accae 100644
--- a/Lib/test/test_importlib/metadata/test_main.py
+++ b/Lib/test/test_importlib/metadata/test_main.py
@@ -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)
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 1451cf3..cacf14a 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -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 \