mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
* Reapply "gh-132947: Apply changes from importlib_metadata 8.7 (#137885)" (#137924)
This reverts commit 3706ef66ef.
* Skip the triggering test on buildbots only.
This commit is contained in:
parent
2a54acf3c3
commit
9b38c6698a
13 changed files with 311 additions and 122 deletions
|
|
@ -1,33 +1,40 @@
|
|||
"""
|
||||
APIs exposing metadata from third-party Python packages.
|
||||
|
||||
This codebase is shared between importlib.metadata in the stdlib
|
||||
and importlib_metadata in PyPI. See
|
||||
https://github.com/python/importlib_metadata/wiki/Development-Methodology
|
||||
for more detail.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import abc
|
||||
import sys
|
||||
import json
|
||||
import collections
|
||||
import email
|
||||
import types
|
||||
import inspect
|
||||
import pathlib
|
||||
import zipfile
|
||||
import operator
|
||||
import textwrap
|
||||
import functools
|
||||
import itertools
|
||||
import operator
|
||||
import os
|
||||
import pathlib
|
||||
import posixpath
|
||||
import collections
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
import types
|
||||
from collections.abc import Iterable, Mapping
|
||||
from contextlib import suppress
|
||||
from importlib import import_module
|
||||
from importlib.abc import MetaPathFinder
|
||||
from itertools import starmap
|
||||
from typing import Any
|
||||
|
||||
from . import _meta
|
||||
from ._collections import FreezableDefaultDict, Pair
|
||||
from ._functools import method_cache, pass_none
|
||||
from ._itertools import always_iterable, bucket, unique_everseen
|
||||
from ._meta import PackageMetadata, SimplePath
|
||||
|
||||
from contextlib import suppress
|
||||
from importlib import import_module
|
||||
from importlib.abc import MetaPathFinder
|
||||
from itertools import starmap
|
||||
from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast
|
||||
from ._typing import md_none
|
||||
|
||||
__all__ = [
|
||||
'Distribution',
|
||||
|
|
@ -53,7 +60,7 @@ def __str__(self) -> str:
|
|||
return f"No package metadata was found for {self.name}"
|
||||
|
||||
@property
|
||||
def name(self) -> str: # type: ignore[override]
|
||||
def name(self) -> str: # type: ignore[override] # make readonly
|
||||
(name,) = self.args
|
||||
return name
|
||||
|
||||
|
|
@ -123,6 +130,12 @@ def valid(line: str):
|
|||
return line and not line.startswith('#')
|
||||
|
||||
|
||||
class _EntryPointMatch(types.SimpleNamespace):
|
||||
module: str
|
||||
attr: str
|
||||
extras: str
|
||||
|
||||
|
||||
class EntryPoint:
|
||||
"""An entry point as defined by Python packaging conventions.
|
||||
|
||||
|
|
@ -138,6 +151,30 @@ class EntryPoint:
|
|||
'attr'
|
||||
>>> ep.extras
|
||||
['extra1', 'extra2']
|
||||
|
||||
If the value package or module are not valid identifiers, a
|
||||
ValueError is raised on access.
|
||||
|
||||
>>> EntryPoint(name=None, group=None, value='invalid-name').module
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: ('Invalid object reference...invalid-name...
|
||||
>>> EntryPoint(name=None, group=None, value='invalid-name').attr
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: ('Invalid object reference...invalid-name...
|
||||
>>> EntryPoint(name=None, group=None, value='invalid-name').extras
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: ('Invalid object reference...invalid-name...
|
||||
|
||||
The same thing happens on construction.
|
||||
|
||||
>>> EntryPoint(name=None, group=None, value='invalid-name')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: ('Invalid object reference...invalid-name...
|
||||
|
||||
"""
|
||||
|
||||
pattern = re.compile(
|
||||
|
|
@ -165,38 +202,44 @@ class EntryPoint:
|
|||
value: str
|
||||
group: str
|
||||
|
||||
dist: Optional[Distribution] = None
|
||||
dist: Distribution | None = None
|
||||
|
||||
def __init__(self, name: str, value: str, group: str) -> None:
|
||||
vars(self).update(name=name, value=value, group=group)
|
||||
self.module
|
||||
|
||||
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 = cast(Match, self.pattern.match(self.value))
|
||||
module = import_module(match.group('module'))
|
||||
attrs = filter(None, (match.group('attr') or '').split('.'))
|
||||
module = import_module(self.module)
|
||||
attrs = filter(None, (self.attr or '').split('.'))
|
||||
return functools.reduce(getattr, attrs, module)
|
||||
|
||||
@property
|
||||
def module(self) -> str:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return match.group('module')
|
||||
return self._match.module
|
||||
|
||||
@property
|
||||
def attr(self) -> str:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return match.group('attr')
|
||||
return self._match.attr
|
||||
|
||||
@property
|
||||
def extras(self) -> List[str]:
|
||||
def extras(self) -> list[str]:
|
||||
return re.findall(r'\w+', self._match.extras or '')
|
||||
|
||||
@functools.cached_property
|
||||
def _match(self) -> _EntryPointMatch:
|
||||
match = self.pattern.match(self.value)
|
||||
assert match is not None
|
||||
return re.findall(r'\w+', match.group('extras') or '')
|
||||
if not match:
|
||||
raise ValueError(
|
||||
'Invalid object reference. '
|
||||
'See https://packaging.python.org'
|
||||
'/en/latest/specifications/entry-points/#data-model',
|
||||
self.value,
|
||||
)
|
||||
return _EntryPointMatch(**match.groupdict())
|
||||
|
||||
def _for(self, dist):
|
||||
vars(self).update(dist=dist)
|
||||
|
|
@ -222,9 +265,26 @@ def matches(self, **params):
|
|||
>>> ep.matches(attr='bong')
|
||||
True
|
||||
"""
|
||||
self._disallow_dist(params)
|
||||
attrs = (getattr(self, param) for param in params)
|
||||
return all(map(operator.eq, params.values(), attrs))
|
||||
|
||||
@staticmethod
|
||||
def _disallow_dist(params):
|
||||
"""
|
||||
Querying by dist is not allowed (dist objects are not comparable).
|
||||
>>> EntryPoint(name='fan', value='fav', group='fag').matches(dist='foo')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: "dist" is not suitable for matching...
|
||||
"""
|
||||
if "dist" in params:
|
||||
raise ValueError(
|
||||
'"dist" is not suitable for matching. '
|
||||
"Instead, use Distribution.entry_points.select() on a "
|
||||
"located distribution."
|
||||
)
|
||||
|
||||
def _key(self):
|
||||
return self.name, self.value, self.group
|
||||
|
||||
|
|
@ -254,7 +314,7 @@ class EntryPoints(tuple):
|
|||
|
||||
__slots__ = ()
|
||||
|
||||
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
|
||||
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int
|
||||
"""
|
||||
Get the EntryPoint in self matching name.
|
||||
"""
|
||||
|
|
@ -278,14 +338,14 @@ def select(self, **params) -> EntryPoints:
|
|||
return EntryPoints(ep for ep in self if ep.matches(**params))
|
||||
|
||||
@property
|
||||
def names(self) -> Set[str]:
|
||||
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) -> Set[str]:
|
||||
def groups(self) -> set[str]:
|
||||
"""
|
||||
Return the set of all groups of all entry points.
|
||||
"""
|
||||
|
|
@ -306,11 +366,11 @@ def _from_text(text):
|
|||
class PackagePath(pathlib.PurePosixPath):
|
||||
"""A reference to a path in a package"""
|
||||
|
||||
hash: Optional[FileHash]
|
||||
hash: FileHash | None
|
||||
size: int
|
||||
dist: Distribution
|
||||
|
||||
def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
|
||||
def read_text(self, encoding: str = 'utf-8') -> str:
|
||||
return self.locate().read_text(encoding=encoding)
|
||||
|
||||
def read_binary(self) -> bytes:
|
||||
|
|
@ -341,7 +401,7 @@ class Distribution(metaclass=abc.ABCMeta):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def read_text(self, filename) -> Optional[str]:
|
||||
def read_text(self, filename) -> str | None:
|
||||
"""Attempt to load metadata file given by the name.
|
||||
|
||||
Python distribution metadata is organized by blobs of text
|
||||
|
|
@ -368,6 +428,17 @@ def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
|
|||
"""
|
||||
Given a path to a file in this distribution, return a SimplePath
|
||||
to it.
|
||||
|
||||
This method is used by callers of ``Distribution.files()`` to
|
||||
locate files within the distribution. If it's possible for a
|
||||
Distribution to represent files in the distribution as
|
||||
``SimplePath`` objects, it should implement this method
|
||||
to resolve such objects.
|
||||
|
||||
Some Distribution providers may elect not to resolve SimplePath
|
||||
objects within the distribution by raising a
|
||||
NotImplementedError, but consumers of such a Distribution would
|
||||
be unable to invoke ``Distribution.files()``.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
|
|
@ -390,7 +461,7 @@ def from_name(cls, name: str) -> Distribution:
|
|||
|
||||
@classmethod
|
||||
def discover(
|
||||
cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
|
||||
cls, *, context: DistributionFinder.Context | None = None, **kwargs
|
||||
) -> Iterable[Distribution]:
|
||||
"""Return an iterable of Distribution objects for all packages.
|
||||
|
||||
|
|
@ -436,7 +507,7 @@ def _discover_resolvers():
|
|||
return filter(None, declared)
|
||||
|
||||
@property
|
||||
def metadata(self) -> _meta.PackageMetadata:
|
||||
def metadata(self) -> _meta.PackageMetadata | None:
|
||||
"""Return the parsed metadata for this Distribution.
|
||||
|
||||
The returned object will have keys that name the various bits of
|
||||
|
|
@ -446,10 +517,8 @@ def metadata(self) -> _meta.PackageMetadata:
|
|||
Custom providers may provide the METADATA file or override this
|
||||
property.
|
||||
"""
|
||||
# deferred for performance (python/cpython#109829)
|
||||
from . import _adapters
|
||||
|
||||
opt_text = (
|
||||
text = (
|
||||
self.read_text('METADATA')
|
||||
or self.read_text('PKG-INFO')
|
||||
# This last clause is here to support old egg-info files. Its
|
||||
|
|
@ -457,13 +526,20 @@ def metadata(self) -> _meta.PackageMetadata:
|
|||
# (which points to the egg-info file) attribute unchanged.
|
||||
or self.read_text('')
|
||||
)
|
||||
text = cast(str, opt_text)
|
||||
return self._assemble_message(text)
|
||||
|
||||
@staticmethod
|
||||
@pass_none
|
||||
def _assemble_message(text: str) -> _meta.PackageMetadata:
|
||||
# deferred for performance (python/cpython#109829)
|
||||
from . import _adapters
|
||||
|
||||
return _adapters.Message(email.message_from_string(text))
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the 'Name' metadata for the distribution package."""
|
||||
return self.metadata['Name']
|
||||
return md_none(self.metadata)['Name']
|
||||
|
||||
@property
|
||||
def _normalized_name(self):
|
||||
|
|
@ -473,7 +549,7 @@ def _normalized_name(self):
|
|||
@property
|
||||
def version(self) -> str:
|
||||
"""Return the 'Version' metadata for the distribution package."""
|
||||
return self.metadata['Version']
|
||||
return md_none(self.metadata)['Version']
|
||||
|
||||
@property
|
||||
def entry_points(self) -> EntryPoints:
|
||||
|
|
@ -486,7 +562,7 @@ def entry_points(self) -> EntryPoints:
|
|||
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
|
||||
|
||||
@property
|
||||
def files(self) -> Optional[List[PackagePath]]:
|
||||
def files(self) -> list[PackagePath] | None:
|
||||
"""Files in this distribution.
|
||||
|
||||
:return: List of PackagePath for this distribution or None
|
||||
|
|
@ -579,7 +655,7 @@ def _read_files_egginfo_sources(self):
|
|||
return text and map('"{}"'.format, text.splitlines())
|
||||
|
||||
@property
|
||||
def requires(self) -> Optional[List[str]]:
|
||||
def requires(self) -> list[str] | None:
|
||||
"""Generated requirements specified for this Distribution"""
|
||||
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
|
||||
return reqs and list(reqs)
|
||||
|
|
@ -635,6 +711,9 @@ def origin(self):
|
|||
return self._load_json('direct_url.json')
|
||||
|
||||
def _load_json(self, filename):
|
||||
# Deferred for performance (python/importlib_metadata#503)
|
||||
import json
|
||||
|
||||
return pass_none(json.loads)(
|
||||
self.read_text(filename),
|
||||
object_hook=lambda data: types.SimpleNamespace(**data),
|
||||
|
|
@ -682,7 +761,7 @@ def __init__(self, **kwargs):
|
|||
vars(self).update(kwargs)
|
||||
|
||||
@property
|
||||
def path(self) -> List[str]:
|
||||
def path(self) -> list[str]:
|
||||
"""
|
||||
The sequence of directory path that a distribution finder
|
||||
should search.
|
||||
|
|
@ -719,7 +798,7 @@ class FastPath:
|
|||
True
|
||||
"""
|
||||
|
||||
@functools.lru_cache() # type: ignore
|
||||
@functools.lru_cache() # type: ignore[misc]
|
||||
def __new__(cls, root):
|
||||
return super().__new__(cls)
|
||||
|
||||
|
|
@ -737,6 +816,9 @@ def children(self):
|
|||
return []
|
||||
|
||||
def zip_children(self):
|
||||
# deferred for performance (python/importlib_metadata#502)
|
||||
import zipfile
|
||||
|
||||
zip_path = zipfile.Path(self.root)
|
||||
names = zip_path.root.namelist()
|
||||
self.joinpath = zip_path.joinpath
|
||||
|
|
@ -831,7 +913,7 @@ class Prepared:
|
|||
normalized = None
|
||||
legacy_normalized = None
|
||||
|
||||
def __init__(self, name: Optional[str]):
|
||||
def __init__(self, name: str | None):
|
||||
self.name = name
|
||||
if name is None:
|
||||
return
|
||||
|
|
@ -894,7 +976,7 @@ def __init__(self, path: SimplePath) -> None:
|
|||
"""
|
||||
self._path = path
|
||||
|
||||
def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
|
||||
def read_text(self, filename: str | os.PathLike[str]) -> str | None:
|
||||
with suppress(
|
||||
FileNotFoundError,
|
||||
IsADirectoryError,
|
||||
|
|
@ -958,7 +1040,7 @@ def distributions(**kwargs) -> Iterable[Distribution]:
|
|||
return Distribution.discover(**kwargs)
|
||||
|
||||
|
||||
def metadata(distribution_name: str) -> _meta.PackageMetadata:
|
||||
def metadata(distribution_name: str) -> _meta.PackageMetadata | None:
|
||||
"""Get the metadata for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
|
|
@ -1001,7 +1083,7 @@ def entry_points(**params) -> EntryPoints:
|
|||
return EntryPoints(eps).select(**params)
|
||||
|
||||
|
||||
def files(distribution_name: str) -> Optional[List[PackagePath]]:
|
||||
def files(distribution_name: str) -> list[PackagePath] | None:
|
||||
"""Return a list of files for the named package.
|
||||
|
||||
:param distribution_name: The name of the distribution package to query.
|
||||
|
|
@ -1010,7 +1092,7 @@ def files(distribution_name: str) -> Optional[List[PackagePath]]:
|
|||
return distribution(distribution_name).files
|
||||
|
||||
|
||||
def requires(distribution_name: str) -> Optional[List[str]]:
|
||||
def requires(distribution_name: str) -> list[str] | None:
|
||||
"""
|
||||
Return a list of requirements for the named package.
|
||||
|
||||
|
|
@ -1020,7 +1102,7 @@ def requires(distribution_name: str) -> Optional[List[str]]:
|
|||
return distribution(distribution_name).requires
|
||||
|
||||
|
||||
def packages_distributions() -> Mapping[str, List[str]]:
|
||||
def packages_distributions() -> Mapping[str, list[str]]:
|
||||
"""
|
||||
Return a mapping of top-level packages to their
|
||||
distributions.
|
||||
|
|
@ -1033,7 +1115,7 @@ def packages_distributions() -> Mapping[str, List[str]]:
|
|||
pkg_to_dist = collections.defaultdict(list)
|
||||
for dist in distributions():
|
||||
for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
|
||||
pkg_to_dist[pkg].append(dist.metadata['Name'])
|
||||
pkg_to_dist[pkg].append(md_none(dist.metadata)['Name'])
|
||||
return dict(pkg_to_dist)
|
||||
|
||||
|
||||
|
|
@ -1041,7 +1123,7 @@ def _top_level_declared(dist):
|
|||
return (dist.read_text('top_level.txt') or '').split()
|
||||
|
||||
|
||||
def _topmost(name: PackagePath) -> Optional[str]:
|
||||
def _topmost(name: PackagePath) -> str | None:
|
||||
"""
|
||||
Return the top-most parent as long as there is a parent.
|
||||
"""
|
||||
|
|
@ -1067,11 +1149,10 @@ def _get_toplevel_name(name: PackagePath) -> str:
|
|||
>>> _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)
|
||||
)
|
||||
# Defer import of inspect for performance (python/cpython#118761)
|
||||
import inspect
|
||||
|
||||
return _topmost(name) or inspect.getmodulename(name) or str(name)
|
||||
|
||||
|
||||
def _top_level_inferred(dist):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,58 @@
|
|||
import email.message
|
||||
import email.policy
|
||||
import re
|
||||
import textwrap
|
||||
import email.message
|
||||
|
||||
from ._text import FoldedCase
|
||||
|
||||
|
||||
class RawPolicy(email.policy.EmailPolicy):
|
||||
def fold(self, name, value):
|
||||
folded = self.linesep.join(
|
||||
textwrap.indent(value, prefix=' ' * 8, predicate=lambda line: True)
|
||||
.lstrip()
|
||||
.splitlines()
|
||||
)
|
||||
return f'{name}: {folded}{self.linesep}'
|
||||
|
||||
|
||||
class Message(email.message.Message):
|
||||
r"""
|
||||
Specialized Message subclass to handle metadata naturally.
|
||||
|
||||
Reads values that may have newlines in them and converts the
|
||||
payload to the Description.
|
||||
|
||||
>>> msg_text = textwrap.dedent('''
|
||||
... Name: Foo
|
||||
... Version: 3.0
|
||||
... License: blah
|
||||
... de-blah
|
||||
... <BLANKLINE>
|
||||
... First line of description.
|
||||
... Second line of description.
|
||||
... <BLANKLINE>
|
||||
... Fourth line!
|
||||
... ''').lstrip().replace('<BLANKLINE>', '')
|
||||
>>> msg = Message(email.message_from_string(msg_text))
|
||||
>>> msg['Description']
|
||||
'First line of description.\nSecond line of description.\n\nFourth line!\n'
|
||||
|
||||
Message should render even if values contain newlines.
|
||||
|
||||
>>> print(msg)
|
||||
Name: Foo
|
||||
Version: 3.0
|
||||
License: blah
|
||||
de-blah
|
||||
Description: First line of description.
|
||||
Second line of description.
|
||||
<BLANKLINE>
|
||||
Fourth line!
|
||||
<BLANKLINE>
|
||||
<BLANKLINE>
|
||||
"""
|
||||
|
||||
multiple_use_keys = set(
|
||||
map(
|
||||
FoldedCase,
|
||||
|
|
@ -57,15 +104,20 @@ def __getitem__(self, item):
|
|||
def _repair_headers(self):
|
||||
def redent(value):
|
||||
"Correct for RFC822 indentation"
|
||||
if not value or '\n' not in value:
|
||||
indent = ' ' * 8
|
||||
if not value or '\n' + indent not in value:
|
||||
return value
|
||||
return textwrap.dedent(' ' * 8 + value)
|
||||
return textwrap.dedent(indent + value)
|
||||
|
||||
headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
|
||||
if self._payload:
|
||||
headers.append(('Description', self.get_payload()))
|
||||
self.set_payload('')
|
||||
return headers
|
||||
|
||||
def as_string(self):
|
||||
return super().as_string(policy=RawPolicy())
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import collections
|
||||
import typing
|
||||
|
||||
|
||||
# from jaraco.collections 3.3
|
||||
|
|
@ -24,7 +25,10 @@ def freeze(self):
|
|||
self._frozen = lambda key: self.default_factory()
|
||||
|
||||
|
||||
class Pair(collections.namedtuple('Pair', 'name value')):
|
||||
class Pair(typing.NamedTuple):
|
||||
name: str
|
||||
value: str
|
||||
|
||||
@classmethod
|
||||
def parse(cls, text):
|
||||
return cls(*map(str.strip, text.split("=", 1)))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import types
|
||||
import functools
|
||||
import types
|
||||
|
||||
|
||||
# from jaraco.functools 3.3
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Protocol
|
||||
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
|
||||
|
||||
from collections.abc import Iterator
|
||||
from typing import (
|
||||
Any,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
overload,
|
||||
)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
|
@ -20,25 +24,25 @@ def __iter__(self) -> Iterator[str]: ... # pragma: no cover
|
|||
@overload
|
||||
def get(
|
||||
self, name: str, failobj: None = None
|
||||
) -> Optional[str]: ... # pragma: no cover
|
||||
) -> str | None: ... # pragma: no cover
|
||||
|
||||
@overload
|
||||
def get(self, name: str, failobj: _T) -> Union[str, _T]: ... # pragma: no cover
|
||||
def get(self, name: str, failobj: _T) -> 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
|
||||
) -> list[Any] | None: ... # pragma: no cover
|
||||
|
||||
@overload
|
||||
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
|
||||
def get_all(self, name: str, failobj: _T) -> list[Any] | _T:
|
||||
"""
|
||||
Return all values associated with a possibly multi-valued key.
|
||||
"""
|
||||
|
||||
@property
|
||||
def json(self) -> Dict[str, Union[str, List[str]]]:
|
||||
def json(self) -> dict[str, str | list[str]]:
|
||||
"""
|
||||
A JSON-compatible form of the metadata.
|
||||
"""
|
||||
|
|
@ -50,11 +54,11 @@ class SimplePath(Protocol):
|
|||
"""
|
||||
|
||||
def joinpath(
|
||||
self, other: Union[str, os.PathLike[str]]
|
||||
self, other: str | os.PathLike[str]
|
||||
) -> SimplePath: ... # pragma: no cover
|
||||
|
||||
def __truediv__(
|
||||
self, other: Union[str, os.PathLike[str]]
|
||||
self, other: str | os.PathLike[str]
|
||||
) -> SimplePath: ... # pragma: no cover
|
||||
|
||||
@property
|
||||
|
|
|
|||
15
Lib/importlib/metadata/_typing.py
Normal file
15
Lib/importlib/metadata/_typing.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import functools
|
||||
import typing
|
||||
|
||||
from ._meta import PackageMetadata
|
||||
|
||||
md_none = functools.partial(typing.cast, PackageMetadata)
|
||||
"""
|
||||
Suppress type errors for optional metadata.
|
||||
|
||||
Although Distribution.metadata can return None when metadata is corrupt
|
||||
and thus None, allow callers to assume it's not None and crash if
|
||||
that's the case.
|
||||
|
||||
# python/importlib_metadata#493
|
||||
"""
|
||||
15
Lib/test/test_importlib/metadata/_issue138313.py
Normal file
15
Lib/test/test_importlib/metadata/_issue138313.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import os
|
||||
import unittest
|
||||
|
||||
|
||||
def skip_on_buildbot(func):
|
||||
"""
|
||||
#132947 revealed that after applying some otherwise stable
|
||||
changes, only on some buildbot runners, the tests will fail with
|
||||
ResourceWarnings.
|
||||
"""
|
||||
# detect "not github actions" as a proxy for BUILDBOT not being present yet.
|
||||
is_buildbot = "GITHUB_ACTION" not in os.environ or "BUILDBOT" in os.environ
|
||||
skipper = unittest.skip("Causes Resource Warnings (python/cpython#132947)")
|
||||
wrapper = skipper if is_buildbot else lambda x: x
|
||||
return wrapper(func)
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
# from jaraco.path 3.7
|
||||
# from jaraco.path 3.7.2
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import pathlib
|
||||
from typing import Dict, Protocol, Union
|
||||
from typing import runtime_checkable
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
class Symlink(str):
|
||||
|
|
@ -12,29 +17,25 @@ class Symlink(str):
|
|||
"""
|
||||
|
||||
|
||||
FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore
|
||||
FilesSpec = Mapping[str, Union[str, bytes, Symlink, 'FilesSpec']]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TreeMaker(Protocol):
|
||||
def __truediv__(self, *args, **kwargs): ... # pragma: no cover
|
||||
|
||||
def mkdir(self, **kwargs): ... # pragma: no cover
|
||||
|
||||
def write_text(self, content, **kwargs): ... # pragma: no cover
|
||||
|
||||
def write_bytes(self, content): ... # pragma: no cover
|
||||
|
||||
def symlink_to(self, target): ... # pragma: no cover
|
||||
def __truediv__(self, other, /) -> Self: ...
|
||||
def mkdir(self, *, exist_ok) -> object: ...
|
||||
def write_text(self, content, /, *, encoding) -> object: ...
|
||||
def write_bytes(self, content, /) -> object: ...
|
||||
def symlink_to(self, target, /) -> object: ...
|
||||
|
||||
|
||||
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
|
||||
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore
|
||||
def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker:
|
||||
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj)
|
||||
|
||||
|
||||
def build(
|
||||
spec: FilesSpec,
|
||||
prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore
|
||||
prefix: str | TreeMaker = pathlib.Path(),
|
||||
):
|
||||
"""
|
||||
Build a set of files/directories, as described by the spec.
|
||||
|
|
@ -66,23 +67,24 @@ def build(
|
|||
|
||||
|
||||
@functools.singledispatch
|
||||
def create(content: Union[str, bytes, FilesSpec], path):
|
||||
def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None:
|
||||
path.mkdir(exist_ok=True)
|
||||
build(content, prefix=path) # type: ignore
|
||||
# Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union
|
||||
build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: bytes, path):
|
||||
def _(content: bytes, path: TreeMaker) -> None:
|
||||
path.write_bytes(content)
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: str, path):
|
||||
def _(content: str, path: TreeMaker) -> None:
|
||||
path.write_text(content, encoding='utf-8')
|
||||
|
||||
|
||||
@create.register
|
||||
def _(content: Symlink, path):
|
||||
def _(content: Symlink, path: TreeMaker) -> None:
|
||||
path.symlink_to(content)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import sys
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import pathlib
|
||||
import textwrap
|
||||
import functools
|
||||
import contextlib
|
||||
import copy
|
||||
import functools
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from test.support import import_helper
|
||||
from test.support import os_helper
|
||||
|
|
@ -14,14 +14,10 @@
|
|||
from . import _path
|
||||
from ._path import FilesSpec
|
||||
|
||||
|
||||
try:
|
||||
from importlib import resources # type: ignore
|
||||
|
||||
getattr(resources, 'files')
|
||||
getattr(resources, 'as_file')
|
||||
except (ImportError, AttributeError):
|
||||
import importlib_resources as resources # type: ignore
|
||||
if sys.version_info >= (3, 9):
|
||||
from importlib import resources
|
||||
else:
|
||||
import importlib_resources as resources
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import importlib
|
||||
import re
|
||||
import textwrap
|
||||
import unittest
|
||||
import importlib
|
||||
|
||||
from . import fixtures
|
||||
from importlib.metadata import (
|
||||
Distribution,
|
||||
PackageNotFoundError,
|
||||
|
|
@ -15,6 +14,8 @@
|
|||
version,
|
||||
)
|
||||
|
||||
from . import fixtures
|
||||
|
||||
|
||||
class APITests(
|
||||
fixtures.EggInfoPkg,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import re
|
||||
import pickle
|
||||
import unittest
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import pickle
|
||||
import re
|
||||
import unittest
|
||||
from test.support import os_helper
|
||||
|
||||
try:
|
||||
|
|
@ -10,8 +9,6 @@
|
|||
except ImportError:
|
||||
from .stubs import fake_filesystem_unittest as ffs
|
||||
|
||||
from . import fixtures
|
||||
from ._path import Symlink
|
||||
from importlib.metadata import (
|
||||
Distribution,
|
||||
EntryPoint,
|
||||
|
|
@ -24,6 +21,10 @@
|
|||
version,
|
||||
)
|
||||
|
||||
from . import fixtures
|
||||
from . import _issue138313
|
||||
from ._path import Symlink
|
||||
|
||||
|
||||
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
|
||||
version_pattern = r'\d+\.\d+(\.\d)?'
|
||||
|
|
@ -157,6 +158,16 @@ def test_valid_dists_preferred(self):
|
|||
dist = Distribution.from_name('foo')
|
||||
assert dist.version == "1.0"
|
||||
|
||||
def test_missing_metadata(self):
|
||||
"""
|
||||
Dists with a missing metadata file should return None.
|
||||
|
||||
Ref python/importlib_metadata#493.
|
||||
"""
|
||||
fixtures.build_files(self.make_pkg('foo-4.3', files={}), self.site_dir)
|
||||
assert Distribution.from_name('foo').metadata is None
|
||||
assert metadata('foo') is None
|
||||
|
||||
|
||||
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||
@staticmethod
|
||||
|
|
@ -347,6 +358,7 @@ def test_packages_distributions_example(self):
|
|||
self._fixture_on_path('example-21.12-py3-none-any.whl')
|
||||
assert packages_distributions()['example'] == ['example']
|
||||
|
||||
@_issue138313.skip_on_buildbot
|
||||
def test_packages_distributions_example2(self):
|
||||
"""
|
||||
Test packages_distributions on a wheel built
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
from . import fixtures
|
||||
from importlib.metadata import (
|
||||
PackageNotFoundError,
|
||||
distribution,
|
||||
|
|
@ -11,6 +10,8 @@
|
|||
version,
|
||||
)
|
||||
|
||||
from . import fixtures
|
||||
|
||||
|
||||
class TestZip(fixtures.ZipFixtures, unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
Applied changes to ``importlib.metadata`` from `importlib_metadata 8.7
|
||||
<https://importlib-metadata.readthedocs.io/en/latest/history.html#v8-7-0>`_,
|
||||
including ``dist`` now disallowed for ``EntryPoints.select``; deferred
|
||||
imports for faster import times; added support for metadata with newlines
|
||||
(python/cpython#119650); and ``metadata()`` function now returns ``None``
|
||||
when a metadata directory is present but no metadata is present.
|
||||
Loading…
Add table
Add a link
Reference in a new issue