mirror of https://github.com/python/cpython
gh-113174: Sync with importlib_metadata 7.0 (#113175)
* Sync with importlib_metadata 7.0.0 * Add blurb * Update docs to reflect changes. * Link datamodel docs for object.__getitem__ Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> * Add what's new for removed __getattr__ * Link datamodel docs for object.__getitem__ Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> * Add exclamation point, as that seems to be used for other classes. --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
6b70c3dc5a
commit
2d91409c69
|
@ -171,16 +171,18 @@ group. Read `the setuptools docs
|
|||
<https://setuptools.pypa.io/en/latest/userguide/entry_point.html>`_
|
||||
for more information on entry points, their definition, and usage.
|
||||
|
||||
*Compatibility Note*
|
||||
|
||||
The "selectable" entry points were introduced in ``importlib_metadata``
|
||||
3.6 and Python 3.10. Prior to those changes, ``entry_points`` accepted
|
||||
no parameters and always returned a dictionary of entry points, keyed
|
||||
by group. With ``importlib_metadata`` 5.0 and Python 3.12,
|
||||
``entry_points`` always returns an ``EntryPoints`` object. See
|
||||
`backports.entry_points_selectable <https://pypi.org/project/backports.entry-points-selectable>`_
|
||||
for compatibility options.
|
||||
.. versionchanged:: 3.12
|
||||
The "selectable" entry points were introduced in ``importlib_metadata``
|
||||
3.6 and Python 3.10. Prior to those changes, ``entry_points`` accepted
|
||||
no parameters and always returned a dictionary of entry points, keyed
|
||||
by group. With ``importlib_metadata`` 5.0 and Python 3.12,
|
||||
``entry_points`` always returns an ``EntryPoints`` object. See
|
||||
`backports.entry_points_selectable <https://pypi.org/project/backports.entry-points-selectable>`_
|
||||
for compatibility options.
|
||||
|
||||
.. versionchanged:: 3.13
|
||||
``EntryPoint`` objects no longer present a tuple-like interface
|
||||
(:meth:`~object.__getitem__`).
|
||||
|
||||
.. _metadata:
|
||||
|
||||
|
@ -342,9 +344,17 @@ instance::
|
|||
>>> dist.metadata['License'] # doctest: +SKIP
|
||||
'MIT'
|
||||
|
||||
For editable packages, an origin property may present :pep:`610`
|
||||
metadata::
|
||||
|
||||
>>> dist.origin.url
|
||||
'file:///path/to/wheel-0.32.3.editable-py3-none-any.whl'
|
||||
|
||||
The full set of available metadata is not described here.
|
||||
See the `Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_ for additional details.
|
||||
|
||||
.. versionadded:: 3.13
|
||||
The ``.origin`` property was added.
|
||||
|
||||
Distribution Discovery
|
||||
======================
|
||||
|
|
|
@ -1001,6 +1001,10 @@ importlib
|
|||
for migration advice.
|
||||
(Contributed by Jason R. Coombs in :gh:`106532`.)
|
||||
|
||||
* Remove deprecated :meth:`~object.__getitem__` access for
|
||||
:class:`!importlib.metadata.EntryPoint` objects.
|
||||
(Contributed by Jason R. Coombs in :gh:`113175`.)
|
||||
|
||||
locale
|
||||
------
|
||||
|
||||
|
|
|
@ -3,7 +3,10 @@ import re
|
|||
import abc
|
||||
import csv
|
||||
import sys
|
||||
import json
|
||||
import email
|
||||
import types
|
||||
import inspect
|
||||
import pathlib
|
||||
import zipfile
|
||||
import operator
|
||||
|
@ -13,7 +16,6 @@ import functools
|
|||
import itertools
|
||||
import posixpath
|
||||
import collections
|
||||
import inspect
|
||||
|
||||
from . import _adapters, _meta
|
||||
from ._collections import FreezableDefaultDict, Pair
|
||||
|
@ -25,8 +27,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, cast
|
||||
|
||||
from typing import Iterable, List, Mapping, Optional, Set, Union, cast
|
||||
|
||||
__all__ = [
|
||||
'Distribution',
|
||||
|
@ -47,11 +48,11 @@ __all__ = [
|
|||
class PackageNotFoundError(ModuleNotFoundError):
|
||||
"""The package was not found."""
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"No package metadata was found for {self.name}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str: # type: ignore[override]
|
||||
(name,) = self.args
|
||||
return name
|
||||
|
||||
|
@ -117,38 +118,11 @@ class Sectioned:
|
|||
yield Pair(name, value)
|
||||
|
||||
@staticmethod
|
||||
def valid(line):
|
||||
def valid(line: str):
|
||||
return line and not line.startswith('#')
|
||||
|
||||
|
||||
class DeprecatedTuple:
|
||||
"""
|
||||
Provide subscript item access for backward compatibility.
|
||||
|
||||
>>> recwarn = getfixture('recwarn')
|
||||
>>> ep = EntryPoint(name='name', value='value', group='group')
|
||||
>>> ep[:]
|
||||
('name', 'value', 'group')
|
||||
>>> ep[0]
|
||||
'name'
|
||||
>>> len(recwarn)
|
||||
1
|
||||
"""
|
||||
|
||||
# Do not remove prior to 2023-05-01 or Python 3.13
|
||||
_warn = functools.partial(
|
||||
warnings.warn,
|
||||
"EntryPoint tuple interface is deprecated. Access members by name.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
def __getitem__(self, item):
|
||||
self._warn()
|
||||
return self._key()[item]
|
||||
|
||||
|
||||
class EntryPoint(DeprecatedTuple):
|
||||
class EntryPoint:
|
||||
"""An entry point as defined by Python packaging conventions.
|
||||
|
||||
See `the packaging docs on entry points
|
||||
|
@ -192,7 +166,7 @@ class EntryPoint(DeprecatedTuple):
|
|||
|
||||
dist: Optional['Distribution'] = None
|
||||
|
||||
def __init__(self, name, value, group):
|
||||
def __init__(self, name: str, value: str, group: str) -> None:
|
||||
vars(self).update(name=name, value=value, group=group)
|
||||
|
||||
def load(self):
|
||||
|
@ -206,18 +180,21 @@ class EntryPoint(DeprecatedTuple):
|
|||
return functools.reduce(getattr, attrs, module)
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
def module(self) -> str:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return match.group('module')
|
||||
|
||||
@property
|
||||
def attr(self):
|
||||
def attr(self) -> str:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return match.group('attr')
|
||||
|
||||
@property
|
||||
def extras(self):
|
||||
def extras(self) -> List[str]:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return re.findall(r'\w+', match.group('extras') or '')
|
||||
|
||||
def _for(self, dist):
|
||||
|
@ -265,7 +242,7 @@ class EntryPoint(DeprecatedTuple):
|
|||
f'group={self.group!r})'
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._key())
|
||||
|
||||
|
||||
|
@ -276,7 +253,7 @@ class EntryPoints(tuple):
|
|||
|
||||
__slots__ = ()
|
||||
|
||||
def __getitem__(self, name): # -> EntryPoint:
|
||||
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
|
||||
"""
|
||||
Get the EntryPoint in self matching name.
|
||||
"""
|
||||
|
@ -285,6 +262,13 @@ class EntryPoints(tuple):
|
|||
except StopIteration:
|
||||
raise KeyError(name)
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Repr with classname and tuple constructor to
|
||||
signal that we deviate from regular tuple behavior.
|
||||
"""
|
||||
return '%s(%r)' % (self.__class__.__name__, tuple(self))
|
||||
|
||||
def select(self, **params):
|
||||
"""
|
||||
Select entry points from self that match the
|
||||
|
@ -293,14 +277,14 @@ class EntryPoints(tuple):
|
|||
return EntryPoints(ep for ep in self if ep.matches(**params))
|
||||
|
||||
@property
|
||||
def names(self):
|
||||
def names(self) -> Set[str]:
|
||||
"""
|
||||
Return the set of all names of all entry points.
|
||||
"""
|
||||
return {ep.name for ep in self}
|
||||
|
||||
@property
|
||||
def groups(self):
|
||||
def groups(self) -> Set[str]:
|
||||
"""
|
||||
Return the set of all groups of all entry points.
|
||||
"""
|
||||
|
@ -321,24 +305,28 @@ class EntryPoints(tuple):
|
|||
class PackagePath(pathlib.PurePosixPath):
|
||||
"""A reference to a path in a package"""
|
||||
|
||||
def read_text(self, encoding='utf-8'):
|
||||
hash: Optional["FileHash"]
|
||||
size: int
|
||||
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()
|
||||
|
||||
def read_binary(self):
|
||||
def read_binary(self) -> bytes:
|
||||
with self.locate().open('rb') as stream:
|
||||
return stream.read()
|
||||
|
||||
def locate(self):
|
||||
def locate(self) -> pathlib.Path:
|
||||
"""Return a path-like object for this path"""
|
||||
return self.dist.locate_file(self)
|
||||
|
||||
|
||||
class FileHash:
|
||||
def __init__(self, spec):
|
||||
def __init__(self, spec: str) -> None:
|
||||
self.mode, _, self.value = spec.partition('=')
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f'<FileHash mode: {self.mode} value: {self.value}>'
|
||||
|
||||
|
||||
|
@ -373,14 +361,14 @@ class Distribution(DeprecatedNonAbstract):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def locate_file(self, path):
|
||||
def locate_file(self, path: Union[str, os.PathLike[str]]) -> pathlib.Path:
|
||||
"""
|
||||
Given a path to a file in this distribution, return a path
|
||||
to it.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_name(cls, name: str):
|
||||
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.
|
||||
|
@ -393,12 +381,12 @@ class Distribution(DeprecatedNonAbstract):
|
|||
if not name:
|
||||
raise ValueError("A distribution name is required.")
|
||||
try:
|
||||
return next(cls.discover(name=name))
|
||||
return next(iter(cls.discover(name=name)))
|
||||
except StopIteration:
|
||||
raise PackageNotFoundError(name)
|
||||
|
||||
@classmethod
|
||||
def discover(cls, **kwargs):
|
||||
def discover(cls, **kwargs) -> Iterable["Distribution"]:
|
||||
"""Return an iterable of Distribution objects for all packages.
|
||||
|
||||
Pass a ``context`` or pass keyword arguments for constructing
|
||||
|
@ -416,7 +404,7 @@ class Distribution(DeprecatedNonAbstract):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def at(path):
|
||||
def at(path: Union[str, os.PathLike[str]]) -> "Distribution":
|
||||
"""Return a Distribution for the indicated metadata path
|
||||
|
||||
:param path: a string or path-like object
|
||||
|
@ -451,7 +439,7 @@ class Distribution(DeprecatedNonAbstract):
|
|||
return _adapters.Message(email.message_from_string(text))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the 'Name' metadata for the distribution package."""
|
||||
return self.metadata['Name']
|
||||
|
||||
|
@ -461,16 +449,16 @@ class Distribution(DeprecatedNonAbstract):
|
|||
return Prepared.normalize(self.name)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
def version(self) -> str:
|
||||
"""Return the 'Version' metadata for the distribution package."""
|
||||
return self.metadata['Version']
|
||||
|
||||
@property
|
||||
def entry_points(self):
|
||||
def entry_points(self) -> EntryPoints:
|
||||
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
def files(self) -> Optional[List[PackagePath]]:
|
||||
"""Files in this distribution.
|
||||
|
||||
:return: List of PackagePath for this distribution or None
|
||||
|
@ -555,7 +543,7 @@ class Distribution(DeprecatedNonAbstract):
|
|||
return text and map('"{}"'.format, text.splitlines())
|
||||
|
||||
@property
|
||||
def requires(self):
|
||||
def requires(self) -> Optional[List[str]]:
|
||||
"""Generated requirements specified for this Distribution"""
|
||||
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
|
||||
return reqs and list(reqs)
|
||||
|
@ -606,6 +594,16 @@ class Distribution(DeprecatedNonAbstract):
|
|||
space = url_req_space(section.value)
|
||||
yield section.value + space + quoted_marker(section.name)
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
return self._load_json('direct_url.json')
|
||||
|
||||
def _load_json(self, filename):
|
||||
return pass_none(json.loads)(
|
||||
self.read_text(filename),
|
||||
object_hook=lambda data: types.SimpleNamespace(**data),
|
||||
)
|
||||
|
||||
|
||||
class DistributionFinder(MetaPathFinder):
|
||||
"""
|
||||
|
@ -634,7 +632,7 @@ class DistributionFinder(MetaPathFinder):
|
|||
vars(self).update(kwargs)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
def path(self) -> List[str]:
|
||||
"""
|
||||
The sequence of directory path that a distribution finder
|
||||
should search.
|
||||
|
@ -645,7 +643,7 @@ class DistributionFinder(MetaPathFinder):
|
|||
return vars(self).get('path', sys.path)
|
||||
|
||||
@abc.abstractmethod
|
||||
def find_distributions(self, context=Context()):
|
||||
def find_distributions(self, context=Context()) -> Iterable[Distribution]:
|
||||
"""
|
||||
Find distributions.
|
||||
|
||||
|
@ -774,7 +772,9 @@ class Prepared:
|
|||
|
||||
class MetadataPathFinder(DistributionFinder):
|
||||
@classmethod
|
||||
def find_distributions(cls, context=DistributionFinder.Context()):
|
||||
def find_distributions(
|
||||
cls, context=DistributionFinder.Context()
|
||||
) -> Iterable["PathDistribution"]:
|
||||
"""
|
||||
Find distributions.
|
||||
|
||||
|
@ -794,19 +794,19 @@ class MetadataPathFinder(DistributionFinder):
|
|||
path.search(prepared) for path in map(FastPath, paths)
|
||||
)
|
||||
|
||||
def invalidate_caches(cls):
|
||||
def invalidate_caches(cls) -> None:
|
||||
FastPath.__new__.cache_clear()
|
||||
|
||||
|
||||
class PathDistribution(Distribution):
|
||||
def __init__(self, path: SimplePath):
|
||||
def __init__(self, path: SimplePath) -> None:
|
||||
"""Construct a distribution.
|
||||
|
||||
:param path: SimplePath indicating the metadata directory.
|
||||
"""
|
||||
self._path = path
|
||||
|
||||
def read_text(self, filename):
|
||||
def read_text(self, filename: Union[str, os.PathLike[str]]) -> Optional[str]:
|
||||
with suppress(
|
||||
FileNotFoundError,
|
||||
IsADirectoryError,
|
||||
|
@ -816,9 +816,11 @@ class PathDistribution(Distribution):
|
|||
):
|
||||
return self._path.joinpath(filename).read_text(encoding='utf-8')
|
||||
|
||||
return None
|
||||
|
||||
read_text.__doc__ = Distribution.read_text.__doc__
|
||||
|
||||
def locate_file(self, path):
|
||||
def locate_file(self, path: Union[str, os.PathLike[str]]) -> pathlib.Path:
|
||||
return self._path.parent / path
|
||||
|
||||
@property
|
||||
|
@ -851,7 +853,7 @@ class PathDistribution(Distribution):
|
|||
return name
|
||||
|
||||
|
||||
def distribution(distribution_name):
|
||||
def distribution(distribution_name: str) -> Distribution:
|
||||
"""Get the ``Distribution`` instance for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package as a string.
|
||||
|
@ -860,7 +862,7 @@ def distribution(distribution_name):
|
|||
return Distribution.from_name(distribution_name)
|
||||
|
||||
|
||||
def distributions(**kwargs):
|
||||
def distributions(**kwargs) -> Iterable[Distribution]:
|
||||
"""Get all ``Distribution`` instances in the current environment.
|
||||
|
||||
:return: An iterable of ``Distribution`` instances.
|
||||
|
@ -868,7 +870,7 @@ def distributions(**kwargs):
|
|||
return Distribution.discover(**kwargs)
|
||||
|
||||
|
||||
def metadata(distribution_name) -> _meta.PackageMetadata:
|
||||
def metadata(distribution_name: str) -> _meta.PackageMetadata:
|
||||
"""Get the metadata for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
|
@ -877,7 +879,7 @@ def metadata(distribution_name) -> _meta.PackageMetadata:
|
|||
return Distribution.from_name(distribution_name).metadata
|
||||
|
||||
|
||||
def version(distribution_name):
|
||||
def version(distribution_name: str) -> str:
|
||||
"""Get the version string for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
|
@ -911,7 +913,7 @@ def entry_points(**params) -> EntryPoints:
|
|||
return EntryPoints(eps).select(**params)
|
||||
|
||||
|
||||
def files(distribution_name):
|
||||
def files(distribution_name: str) -> Optional[List[PackagePath]]:
|
||||
"""Return a list of files for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
|
@ -920,11 +922,11 @@ def files(distribution_name):
|
|||
return distribution(distribution_name).files
|
||||
|
||||
|
||||
def requires(distribution_name):
|
||||
def requires(distribution_name: str) -> Optional[List[str]]:
|
||||
"""
|
||||
Return a list of requirements for the named package.
|
||||
|
||||
:return: An iterator of requirements, suitable for
|
||||
:return: An iterable of requirements, suitable for
|
||||
packaging.requirement.Requirement.
|
||||
"""
|
||||
return distribution(distribution_name).requires
|
||||
|
@ -951,13 +953,42 @@ def _top_level_declared(dist):
|
|||
return (dist.read_text('top_level.txt') or '').split()
|
||||
|
||||
|
||||
def _top_level_inferred(dist):
|
||||
opt_names = {
|
||||
f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
|
||||
for f in always_iterable(dist.files)
|
||||
}
|
||||
def _topmost(name: PackagePath) -> Optional[str]:
|
||||
"""
|
||||
Return the top-most parent as long as there is a parent.
|
||||
"""
|
||||
top, *rest = name.parts
|
||||
return top if rest else None
|
||||
|
||||
|
||||
def _get_toplevel_name(name: PackagePath) -> str:
|
||||
"""
|
||||
Infer a possibly importable module name from a name presumed on
|
||||
sys.path.
|
||||
|
||||
>>> _get_toplevel_name(PackagePath('foo.py'))
|
||||
'foo'
|
||||
>>> _get_toplevel_name(PackagePath('foo'))
|
||||
'foo'
|
||||
>>> _get_toplevel_name(PackagePath('foo.pyc'))
|
||||
'foo'
|
||||
>>> _get_toplevel_name(PackagePath('foo/__init__.py'))
|
||||
'foo'
|
||||
>>> _get_toplevel_name(PackagePath('foo.pth'))
|
||||
'foo.pth'
|
||||
>>> _get_toplevel_name(PackagePath('foo.dist-info'))
|
||||
'foo.dist-info'
|
||||
"""
|
||||
return _topmost(name) or (
|
||||
# python/typeshed#10328
|
||||
inspect.getmodulename(name) # type: ignore
|
||||
or str(name)
|
||||
)
|
||||
|
||||
|
||||
def _top_level_inferred(dist):
|
||||
opt_names = set(map(_get_toplevel_name, always_iterable(dist.files)))
|
||||
|
||||
@pass_none
|
||||
def importable_name(name):
|
||||
return '.' not in name
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ class Message(email.message.Message):
|
|||
def __getitem__(self, item):
|
||||
"""
|
||||
Warn users that a ``KeyError`` can be expected when a
|
||||
mising key is supplied. Ref python/importlib_metadata#371.
|
||||
missing key is supplied. Ref python/importlib_metadata#371.
|
||||
"""
|
||||
res = super().__getitem__(item)
|
||||
if res is None:
|
||||
|
|
|
@ -49,7 +49,7 @@ class SimplePath(Protocol[_T]):
|
|||
A minimal subset of pathlib.Path required by PathDistribution.
|
||||
"""
|
||||
|
||||
def joinpath(self) -> _T:
|
||||
def joinpath(self, other: Union[str, _T]) -> _T:
|
||||
... # pragma: no cover
|
||||
|
||||
def __truediv__(self, other: Union[str, _T]) -> _T:
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import sys
|
||||
|
||||
from . import Distribution
|
||||
|
||||
|
||||
def inspect(path):
|
||||
print("Inspecting", path)
|
||||
dists = list(Distribution.discover(path=[path]))
|
||||
if not dists:
|
||||
return
|
||||
print("Found", len(dists), "packages:", end=' ')
|
||||
print(', '.join(dist.name for dist in dists))
|
||||
|
||||
|
||||
def run():
|
||||
for path in sys.path:
|
||||
inspect(path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
|
@ -1,17 +1,18 @@
|
|||
# from jaraco.path 3.5
|
||||
# from jaraco.path 3.7
|
||||
|
||||
import functools
|
||||
import pathlib
|
||||
from typing import Dict, Union
|
||||
|
||||
try:
|
||||
from typing import Protocol, runtime_checkable
|
||||
except ImportError: # pragma: no cover
|
||||
# Python 3.7
|
||||
from typing_extensions import Protocol, runtime_checkable # type: ignore
|
||||
from typing import Dict, Protocol, Union
|
||||
from typing import runtime_checkable
|
||||
|
||||
|
||||
FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore
|
||||
class Symlink(str):
|
||||
"""
|
||||
A string indicating the target of a symlink.
|
||||
"""
|
||||
|
||||
|
||||
FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
|
@ -28,6 +29,9 @@ class TreeMaker(Protocol):
|
|||
def write_bytes(self, content):
|
||||
... # pragma: no cover
|
||||
|
||||
def symlink_to(self, target):
|
||||
... # pragma: no cover
|
||||
|
||||
|
||||
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
|
||||
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore
|
||||
|
@ -51,12 +55,16 @@ def build(
|
|||
... "__init__.py": "",
|
||||
... },
|
||||
... "baz.py": "# Some code",
|
||||
... }
|
||||
... "bar.py": Symlink("baz.py"),
|
||||
... },
|
||||
... "bing": Symlink("foo"),
|
||||
... }
|
||||
>>> target = getfixture('tmp_path')
|
||||
>>> build(spec, target)
|
||||
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
|
||||
'# Some code'
|
||||
>>> target.joinpath('bing/bar.py').read_text(encoding='utf-8')
|
||||
'# Some code'
|
||||
"""
|
||||
for name, contents in spec.items():
|
||||
create(contents, _ensure_tree_maker(prefix) / name)
|
||||
|
@ -79,8 +87,8 @@ def _(content: str, path):
|
|||
|
||||
|
||||
@create.register
|
||||
def _(content: str, path):
|
||||
path.write_text(content, encoding='utf-8')
|
||||
def _(content: Symlink, path):
|
||||
path.symlink_to(content)
|
||||
|
||||
|
||||
class Recording:
|
||||
|
@ -107,3 +115,6 @@ class Recording:
|
|||
|
||||
def mkdir(self, **kwargs):
|
||||
return
|
||||
|
||||
def symlink_to(self, target):
|
||||
pass
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import sys
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
@ -86,7 +87,15 @@ class OnSysPath(Fixtures):
|
|||
self.fixtures.enter_context(self.add_sys_path(self.site_dir))
|
||||
|
||||
|
||||
class DistInfoPkg(OnSysPath, SiteDir):
|
||||
class SiteBuilder(SiteDir):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
for cls in self.__class__.mro():
|
||||
with contextlib.suppress(AttributeError):
|
||||
build_files(cls.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
class DistInfoPkg(OnSysPath, SiteBuilder):
|
||||
files: FilesSpec = {
|
||||
"distinfo_pkg-1.0.0.dist-info": {
|
||||
"METADATA": """
|
||||
|
@ -113,10 +122,6 @@ class DistInfoPkg(OnSysPath, SiteDir):
|
|||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(DistInfoPkg.files, self.site_dir)
|
||||
|
||||
def make_uppercase(self):
|
||||
"""
|
||||
Rewrite metadata with everything uppercase.
|
||||
|
@ -128,7 +133,28 @@ class DistInfoPkg(OnSysPath, SiteDir):
|
|||
build_files(files, self.site_dir)
|
||||
|
||||
|
||||
class DistInfoPkgWithDot(OnSysPath, SiteDir):
|
||||
class DistInfoPkgEditable(DistInfoPkg):
|
||||
"""
|
||||
Package with a PEP 660 direct_url.json.
|
||||
"""
|
||||
|
||||
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",
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class DistInfoPkgWithDot(OnSysPath, SiteBuilder):
|
||||
files: FilesSpec = {
|
||||
"pkg_dot-1.0.0.dist-info": {
|
||||
"METADATA": """
|
||||
|
@ -138,12 +164,8 @@ class DistInfoPkgWithDot(OnSysPath, SiteDir):
|
|||
},
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(DistInfoPkgWithDot.files, self.site_dir)
|
||||
|
||||
|
||||
class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
|
||||
class DistInfoPkgWithDotLegacy(OnSysPath, SiteBuilder):
|
||||
files: FilesSpec = {
|
||||
"pkg.dot-1.0.0.dist-info": {
|
||||
"METADATA": """
|
||||
|
@ -159,18 +181,12 @@ class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
|
|||
},
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(DistInfoPkgWithDotLegacy.files, self.site_dir)
|
||||
|
||||
class DistInfoPkgOffPath(SiteBuilder):
|
||||
files = DistInfoPkg.files
|
||||
|
||||
|
||||
class DistInfoPkgOffPath(SiteDir):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(DistInfoPkg.files, self.site_dir)
|
||||
|
||||
|
||||
class EggInfoPkg(OnSysPath, SiteDir):
|
||||
class EggInfoPkg(OnSysPath, SiteBuilder):
|
||||
files: FilesSpec = {
|
||||
"egginfo_pkg.egg-info": {
|
||||
"PKG-INFO": """
|
||||
|
@ -205,12 +221,8 @@ class EggInfoPkg(OnSysPath, SiteDir):
|
|||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoPkg.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir):
|
||||
class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteBuilder):
|
||||
files: FilesSpec = {
|
||||
"egg_with_module_pkg.egg-info": {
|
||||
"PKG-INFO": "Name: egg_with_module-pkg",
|
||||
|
@ -240,12 +252,8 @@ class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir):
|
|||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoPkgPipInstalledNoToplevel.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir):
|
||||
class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder):
|
||||
files: FilesSpec = {
|
||||
"egg_with_no_modules_pkg.egg-info": {
|
||||
"PKG-INFO": "Name: egg_with_no_modules-pkg",
|
||||
|
@ -270,12 +278,8 @@ class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir):
|
|||
},
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoPkgPipInstalledNoModules.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
class EggInfoPkgSourcesFallback(OnSysPath, SiteDir):
|
||||
class EggInfoPkgSourcesFallback(OnSysPath, SiteBuilder):
|
||||
files: FilesSpec = {
|
||||
"sources_fallback_pkg.egg-info": {
|
||||
"PKG-INFO": "Name: sources_fallback-pkg",
|
||||
|
@ -296,12 +300,8 @@ class EggInfoPkgSourcesFallback(OnSysPath, SiteDir):
|
|||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoPkgSourcesFallback.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
class EggInfoFile(OnSysPath, SiteDir):
|
||||
class EggInfoFile(OnSysPath, SiteBuilder):
|
||||
files: FilesSpec = {
|
||||
"egginfo_file.egg-info": """
|
||||
Metadata-Version: 1.0
|
||||
|
@ -317,10 +317,6 @@ class EggInfoFile(OnSysPath, SiteDir):
|
|||
""",
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
build_files(EggInfoFile.files, prefix=self.site_dir)
|
||||
|
||||
|
||||
# dedent all text strings before writing
|
||||
orig = _path.create.registry[str]
|
||||
|
|
|
@ -12,6 +12,7 @@ except ImportError:
|
|||
|
||||
from . import fixtures
|
||||
from ._context import suppress
|
||||
from ._path import Symlink
|
||||
from importlib.metadata import (
|
||||
Distribution,
|
||||
EntryPoint,
|
||||
|
@ -68,7 +69,7 @@ class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
|
|||
dict(name=''),
|
||||
)
|
||||
def test_invalid_inputs_to_from_name(self, name):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(Exception):
|
||||
Distribution.from_name(name)
|
||||
|
||||
|
||||
|
@ -207,6 +208,20 @@ class DiscoveryTests(
|
|||
with self.assertRaises(ValueError):
|
||||
list(distributions(context='something', name='else'))
|
||||
|
||||
def test_interleaved_discovery(self):
|
||||
"""
|
||||
Ensure interleaved searches are safe.
|
||||
|
||||
When the search is cached, it is possible for searches to be
|
||||
interleaved, so make sure those use-cases are safe.
|
||||
|
||||
Ref #293
|
||||
"""
|
||||
dists = distributions()
|
||||
next(dists)
|
||||
version('egginfo-pkg')
|
||||
next(dists)
|
||||
|
||||
|
||||
class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||
def test_egg_info(self):
|
||||
|
@ -388,6 +403,27 @@ class PackagesDistributionsTest(
|
|||
|
||||
assert not any(name.endswith('.dist-info') for name in distributions)
|
||||
|
||||
def test_packages_distributions_symlinked_top_level(self) -> None:
|
||||
"""
|
||||
Distribution is resolvable from a simple top-level symlink in RECORD.
|
||||
See #452.
|
||||
"""
|
||||
|
||||
files: fixtures.FilesSpec = {
|
||||
"symlinked_pkg-1.0.0.dist-info": {
|
||||
"METADATA": """
|
||||
Name: symlinked-pkg
|
||||
Version: 1.0.0
|
||||
""",
|
||||
"RECORD": "symlinked,,\n",
|
||||
},
|
||||
".symlink.target": {},
|
||||
"symlinked": Symlink(".symlink.target"),
|
||||
}
|
||||
|
||||
fixtures.build_files(files, self.site_dir)
|
||||
assert packages_distributions()['symlinked'] == ['symlinked-pkg']
|
||||
|
||||
|
||||
class PackagesDistributionsEggTest(
|
||||
fixtures.EggInfoPkg,
|
||||
|
@ -424,3 +460,10 @@ class PackagesDistributionsEggTest(
|
|||
# sources_fallback-pkg has one import ('sources_fallback') inferred from
|
||||
# SOURCES.txt (top_level.txt and installed-files.txt is missing)
|
||||
assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'}
|
||||
|
||||
|
||||
class EditableDistributionTest(fixtures.DistInfoPkgEditable, unittest.TestCase):
|
||||
def test_origin(self):
|
||||
dist = Distribution.from_name('distinfo-pkg')
|
||||
assert dist.origin.url.endswith('.whl')
|
||||
assert dist.origin.archive_info.hashes.sha256
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
Sync with importlib_metadata 7.0, including improved type annotations, fixed
|
||||
issue with symlinked packages in ``package_distributions``, added
|
||||
``EntryPoints.__repr__``, introduced the ``diagnose`` script, added
|
||||
``Distribution.origin`` property, and removed deprecated ``EntryPoint``
|
||||
access by numeric index (tuple behavior).
|
Loading…
Reference in New Issue