Revert "gh-132947: Apply changes from importlib_metadata 8.7 (#137885)" (#137924)
Some checks are pending
Tests / Windows MSI (push) Blocked by required conditions
Tests / Hypothesis tests on Ubuntu (push) Blocked by required conditions
Tests / Address sanitizer (push) Blocked by required conditions
Tests / Sanitizers (push) Blocked by required conditions
Tests / Change detection (push) Waiting to run
Tests / Docs (push) Blocked by required conditions
Tests / Check if Autoconf files are up to date (push) Blocked by required conditions
Tests / Check if generated files are up to date (push) Blocked by required conditions
Tests / (push) Blocked by required conditions
Tests / Ubuntu SSL tests with OpenSSL (push) Blocked by required conditions
Tests / Ubuntu SSL tests with AWS-LC (push) Blocked by required conditions
Tests / Android (aarch64) (push) Blocked by required conditions
Tests / Android (x86_64) (push) Blocked by required conditions
Tests / WASI (push) Blocked by required conditions
Tests / Cross build Linux (push) Blocked by required conditions
Tests / CIFuzz (push) Blocked by required conditions
Tests / All required checks pass (push) Blocked by required conditions
Lint / lint (push) Waiting to run
mypy / Run mypy on Lib/tomllib (push) Waiting to run
mypy / Run mypy on Lib/_pyrepl (push) Waiting to run
mypy / Run mypy on Lib/test/libregrtest (push) Waiting to run
mypy / Run mypy on Tools/build (push) Waiting to run
mypy / Run mypy on Tools/cases_generator (push) Waiting to run
mypy / Run mypy on Tools/clinic (push) Waiting to run
mypy / Run mypy on Tools/jit (push) Waiting to run
mypy / Run mypy on Tools/peg_generator (push) Waiting to run

This reverts commit 5292fc00f2.
This commit is contained in:
Jason R. Coombs 2025-08-18 13:57:36 -07:00 committed by GitHub
parent 8750e5ecfc
commit 3706ef66ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 120 additions and 292 deletions

View file

@ -1,40 +1,33 @@
"""
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 from __future__ import annotations
import os
import re
import abc import abc
import collections import sys
import json
import email import email
import types
import inspect
import pathlib
import zipfile
import operator
import textwrap
import functools import functools
import itertools import itertools
import operator
import os
import pathlib
import posixpath import posixpath
import re import collections
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 . import _meta
from ._collections import FreezableDefaultDict, Pair from ._collections import FreezableDefaultDict, Pair
from ._functools import method_cache, pass_none from ._functools import method_cache, pass_none
from ._itertools import always_iterable, bucket, unique_everseen from ._itertools import always_iterable, bucket, unique_everseen
from ._meta import PackageMetadata, SimplePath from ._meta import PackageMetadata, SimplePath
from ._typing import md_none
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
__all__ = [ __all__ = [
'Distribution', 'Distribution',
@ -60,7 +53,7 @@ class PackageNotFoundError(ModuleNotFoundError):
return f"No package metadata was found for {self.name}" return f"No package metadata was found for {self.name}"
@property @property
def name(self) -> str: # type: ignore[override] # make readonly def name(self) -> str: # type: ignore[override]
(name,) = self.args (name,) = self.args
return name return name
@ -130,12 +123,6 @@ class Sectioned:
return line and not line.startswith('#') return line and not line.startswith('#')
class _EntryPointMatch(types.SimpleNamespace):
module: str
attr: str
extras: str
class EntryPoint: class EntryPoint:
"""An entry point as defined by Python packaging conventions. """An entry point as defined by Python packaging conventions.
@ -151,30 +138,6 @@ class EntryPoint:
'attr' 'attr'
>>> ep.extras >>> ep.extras
['extra1', 'extra2'] ['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( pattern = re.compile(
@ -202,44 +165,38 @@ class EntryPoint:
value: str value: str
group: str group: str
dist: Distribution | None = None dist: Optional[Distribution] = None
def __init__(self, name: str, value: str, group: str) -> None: def __init__(self, name: str, value: str, group: str) -> None:
vars(self).update(name=name, value=value, group=group) vars(self).update(name=name, value=value, group=group)
self.module
def load(self) -> Any: def load(self) -> Any:
"""Load the entry point from its definition. If only a module """Load the entry point from its definition. If only a module
is indicated by the value, return that module. Otherwise, is indicated by the value, return that module. Otherwise,
return the named object. return the named object.
""" """
module = import_module(self.module) match = cast(Match, self.pattern.match(self.value))
attrs = filter(None, (self.attr or '').split('.')) module = import_module(match.group('module'))
attrs = filter(None, (match.group('attr') or '').split('.'))
return functools.reduce(getattr, attrs, module) return functools.reduce(getattr, attrs, module)
@property @property
def module(self) -> str: def module(self) -> str:
return self._match.module match = self.pattern.match(self.value)
assert match is not None
return match.group('module')
@property @property
def attr(self) -> str: def attr(self) -> str:
return self._match.attr match = self.pattern.match(self.value)
assert match is not None
return match.group('attr')
@property @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) match = self.pattern.match(self.value)
if not match: assert match is not None
raise ValueError( return re.findall(r'\w+', match.group('extras') or '')
'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): def _for(self, dist):
vars(self).update(dist=dist) vars(self).update(dist=dist)
@ -265,26 +222,9 @@ class EntryPoint:
>>> ep.matches(attr='bong') >>> ep.matches(attr='bong')
True True
""" """
self._disallow_dist(params)
attrs = (getattr(self, param) for param in params) attrs = (getattr(self, param) for param in params)
return all(map(operator.eq, params.values(), attrs)) 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): def _key(self):
return self.name, self.value, self.group return self.name, self.value, self.group
@ -314,7 +254,7 @@ class EntryPoints(tuple):
__slots__ = () __slots__ = ()
def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
""" """
Get the EntryPoint in self matching name. Get the EntryPoint in self matching name.
""" """
@ -338,14 +278,14 @@ class EntryPoints(tuple):
return EntryPoints(ep for ep in self if ep.matches(**params)) return EntryPoints(ep for ep in self if ep.matches(**params))
@property @property
def names(self) -> set[str]: def names(self) -> Set[str]:
""" """
Return the set of all names of all entry points. Return the set of all names of all entry points.
""" """
return {ep.name for ep in self} return {ep.name for ep in self}
@property @property
def groups(self) -> set[str]: def groups(self) -> Set[str]:
""" """
Return the set of all groups of all entry points. Return the set of all groups of all entry points.
""" """
@ -366,11 +306,11 @@ class EntryPoints(tuple):
class PackagePath(pathlib.PurePosixPath): class PackagePath(pathlib.PurePosixPath):
"""A reference to a path in a package""" """A reference to a path in a package"""
hash: FileHash | None hash: Optional[FileHash]
size: int size: int
dist: Distribution dist: Distribution
def read_text(self, encoding: str = 'utf-8') -> str: def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
return self.locate().read_text(encoding=encoding) return self.locate().read_text(encoding=encoding)
def read_binary(self) -> bytes: def read_binary(self) -> bytes:
@ -401,7 +341,7 @@ class Distribution(metaclass=abc.ABCMeta):
""" """
@abc.abstractmethod @abc.abstractmethod
def read_text(self, filename) -> str | None: def read_text(self, filename) -> Optional[str]:
"""Attempt to load metadata file given by the name. """Attempt to load metadata file given by the name.
Python distribution metadata is organized by blobs of text Python distribution metadata is organized by blobs of text
@ -428,17 +368,6 @@ class Distribution(metaclass=abc.ABCMeta):
""" """
Given a path to a file in this distribution, return a SimplePath Given a path to a file in this distribution, return a SimplePath
to it. 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 @classmethod
@ -461,7 +390,7 @@ class Distribution(metaclass=abc.ABCMeta):
@classmethod @classmethod
def discover( def discover(
cls, *, context: DistributionFinder.Context | None = None, **kwargs cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
) -> Iterable[Distribution]: ) -> Iterable[Distribution]:
"""Return an iterable of Distribution objects for all packages. """Return an iterable of Distribution objects for all packages.
@ -507,7 +436,7 @@ class Distribution(metaclass=abc.ABCMeta):
return filter(None, declared) return filter(None, declared)
@property @property
def metadata(self) -> _meta.PackageMetadata | None: def metadata(self) -> _meta.PackageMetadata:
"""Return the parsed metadata for this Distribution. """Return the parsed metadata for this Distribution.
The returned object will have keys that name the various bits of The returned object will have keys that name the various bits of
@ -517,8 +446,10 @@ class Distribution(metaclass=abc.ABCMeta):
Custom providers may provide the METADATA file or override this Custom providers may provide the METADATA file or override this
property. property.
""" """
# deferred for performance (python/cpython#109829)
from . import _adapters
text = ( opt_text = (
self.read_text('METADATA') self.read_text('METADATA')
or self.read_text('PKG-INFO') or self.read_text('PKG-INFO')
# This last clause is here to support old egg-info files. Its # This last clause is here to support old egg-info files. Its
@ -526,20 +457,13 @@ class Distribution(metaclass=abc.ABCMeta):
# (which points to the egg-info file) attribute unchanged. # (which points to the egg-info file) attribute unchanged.
or self.read_text('') or self.read_text('')
) )
return self._assemble_message(text) text = cast(str, opt_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)) return _adapters.Message(email.message_from_string(text))
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the 'Name' metadata for the distribution package.""" """Return the 'Name' metadata for the distribution package."""
return md_none(self.metadata)['Name'] return self.metadata['Name']
@property @property
def _normalized_name(self): def _normalized_name(self):
@ -549,7 +473,7 @@ class Distribution(metaclass=abc.ABCMeta):
@property @property
def version(self) -> str: def version(self) -> str:
"""Return the 'Version' metadata for the distribution package.""" """Return the 'Version' metadata for the distribution package."""
return md_none(self.metadata)['Version'] return self.metadata['Version']
@property @property
def entry_points(self) -> EntryPoints: def entry_points(self) -> EntryPoints:
@ -562,7 +486,7 @@ class Distribution(metaclass=abc.ABCMeta):
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
@property @property
def files(self) -> list[PackagePath] | None: def files(self) -> Optional[List[PackagePath]]:
"""Files in this distribution. """Files in this distribution.
:return: List of PackagePath for this distribution or None :return: List of PackagePath for this distribution or None
@ -655,7 +579,7 @@ class Distribution(metaclass=abc.ABCMeta):
return text and map('"{}"'.format, text.splitlines()) return text and map('"{}"'.format, text.splitlines())
@property @property
def requires(self) -> list[str] | None: def requires(self) -> Optional[List[str]]:
"""Generated requirements specified for this Distribution""" """Generated requirements specified for this Distribution"""
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
return reqs and list(reqs) return reqs and list(reqs)
@ -711,9 +635,6 @@ class Distribution(metaclass=abc.ABCMeta):
return self._load_json('direct_url.json') return self._load_json('direct_url.json')
def _load_json(self, filename): def _load_json(self, filename):
# Deferred for performance (python/importlib_metadata#503)
import json
return pass_none(json.loads)( return pass_none(json.loads)(
self.read_text(filename), self.read_text(filename),
object_hook=lambda data: types.SimpleNamespace(**data), object_hook=lambda data: types.SimpleNamespace(**data),
@ -761,7 +682,7 @@ class DistributionFinder(MetaPathFinder):
vars(self).update(kwargs) vars(self).update(kwargs)
@property @property
def path(self) -> list[str]: def path(self) -> List[str]:
""" """
The sequence of directory path that a distribution finder The sequence of directory path that a distribution finder
should search. should search.
@ -798,7 +719,7 @@ class FastPath:
True True
""" """
@functools.lru_cache() # type: ignore[misc] @functools.lru_cache() # type: ignore
def __new__(cls, root): def __new__(cls, root):
return super().__new__(cls) return super().__new__(cls)
@ -816,9 +737,6 @@ class FastPath:
return [] return []
def zip_children(self): def zip_children(self):
# deferred for performance (python/importlib_metadata#502)
import zipfile
zip_path = zipfile.Path(self.root) zip_path = zipfile.Path(self.root)
names = zip_path.root.namelist() names = zip_path.root.namelist()
self.joinpath = zip_path.joinpath self.joinpath = zip_path.joinpath
@ -913,7 +831,7 @@ class Prepared:
normalized = None normalized = None
legacy_normalized = None legacy_normalized = None
def __init__(self, name: str | None): def __init__(self, name: Optional[str]):
self.name = name self.name = name
if name is None: if name is None:
return return
@ -976,7 +894,7 @@ class PathDistribution(Distribution):
""" """
self._path = path self._path = path
def read_text(self, filename: str | os.PathLike[str]) -> str | None: def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
with suppress( with suppress(
FileNotFoundError, FileNotFoundError,
IsADirectoryError, IsADirectoryError,
@ -1040,7 +958,7 @@ def distributions(**kwargs) -> Iterable[Distribution]:
return Distribution.discover(**kwargs) return Distribution.discover(**kwargs)
def metadata(distribution_name: str) -> _meta.PackageMetadata | None: def metadata(distribution_name: str) -> _meta.PackageMetadata:
"""Get the metadata for the named package. """Get the metadata for the named package.
:param distribution_name: The name of the distribution package to query. :param distribution_name: The name of the distribution package to query.
@ -1083,7 +1001,7 @@ def entry_points(**params) -> EntryPoints:
return EntryPoints(eps).select(**params) return EntryPoints(eps).select(**params)
def files(distribution_name: str) -> list[PackagePath] | None: def files(distribution_name: str) -> Optional[List[PackagePath]]:
"""Return a list of files for the named package. """Return a list of files for the named package.
:param distribution_name: The name of the distribution package to query. :param distribution_name: The name of the distribution package to query.
@ -1092,7 +1010,7 @@ def files(distribution_name: str) -> list[PackagePath] | None:
return distribution(distribution_name).files return distribution(distribution_name).files
def requires(distribution_name: str) -> list[str] | None: def requires(distribution_name: str) -> Optional[List[str]]:
""" """
Return a list of requirements for the named package. Return a list of requirements for the named package.
@ -1102,7 +1020,7 @@ def requires(distribution_name: str) -> list[str] | None:
return distribution(distribution_name).requires 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 Return a mapping of top-level packages to their
distributions. distributions.
@ -1115,7 +1033,7 @@ def packages_distributions() -> Mapping[str, list[str]]:
pkg_to_dist = collections.defaultdict(list) pkg_to_dist = collections.defaultdict(list)
for dist in distributions(): for dist in distributions():
for pkg in _top_level_declared(dist) or _top_level_inferred(dist): for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
pkg_to_dist[pkg].append(md_none(dist.metadata)['Name']) pkg_to_dist[pkg].append(dist.metadata['Name'])
return dict(pkg_to_dist) return dict(pkg_to_dist)
@ -1123,7 +1041,7 @@ def _top_level_declared(dist):
return (dist.read_text('top_level.txt') or '').split() return (dist.read_text('top_level.txt') or '').split()
def _topmost(name: PackagePath) -> str | None: def _topmost(name: PackagePath) -> Optional[str]:
""" """
Return the top-most parent as long as there is a parent. Return the top-most parent as long as there is a parent.
""" """
@ -1149,10 +1067,11 @@ def _get_toplevel_name(name: PackagePath) -> str:
>>> _get_toplevel_name(PackagePath('foo.dist-info')) >>> _get_toplevel_name(PackagePath('foo.dist-info'))
'foo.dist-info' 'foo.dist-info'
""" """
# Defer import of inspect for performance (python/cpython#118761) return _topmost(name) or (
import inspect # python/typeshed#10328
inspect.getmodulename(name) # type: ignore
return _topmost(name) or inspect.getmodulename(name) or str(name) or str(name)
)
def _top_level_inferred(dist): def _top_level_inferred(dist):

View file

@ -1,58 +1,11 @@
import email.message
import email.policy
import re import re
import textwrap import textwrap
import email.message
from ._text import FoldedCase 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): 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( multiple_use_keys = set(
map( map(
FoldedCase, FoldedCase,
@ -104,20 +57,15 @@ class Message(email.message.Message):
def _repair_headers(self): def _repair_headers(self):
def redent(value): def redent(value):
"Correct for RFC822 indentation" "Correct for RFC822 indentation"
indent = ' ' * 8 if not value or '\n' not in value:
if not value or '\n' + indent not in value:
return value return value
return textwrap.dedent(indent + value) return textwrap.dedent(' ' * 8 + value)
headers = [(key, redent(value)) for key, value in vars(self)['_headers']] headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
if self._payload: if self._payload:
headers.append(('Description', self.get_payload())) headers.append(('Description', self.get_payload()))
self.set_payload('')
return headers return headers
def as_string(self):
return super().as_string(policy=RawPolicy())
@property @property
def json(self): def json(self):
""" """

View file

@ -1,5 +1,4 @@
import collections import collections
import typing
# from jaraco.collections 3.3 # from jaraco.collections 3.3
@ -25,10 +24,7 @@ class FreezableDefaultDict(collections.defaultdict):
self._frozen = lambda key: self.default_factory() self._frozen = lambda key: self.default_factory()
class Pair(typing.NamedTuple): class Pair(collections.namedtuple('Pair', 'name value')):
name: str
value: str
@classmethod @classmethod
def parse(cls, text): def parse(cls, text):
return cls(*map(str.strip, text.split("=", 1))) return cls(*map(str.strip, text.split("=", 1)))

View file

@ -1,5 +1,5 @@
import functools
import types import types
import functools
# from jaraco.functools 3.3 # from jaraco.functools 3.3

View file

@ -1,13 +1,9 @@
from __future__ import annotations from __future__ import annotations
import os import os
from collections.abc import Iterator from typing import Protocol
from typing import ( from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
Any,
Protocol,
TypeVar,
overload,
)
_T = TypeVar("_T") _T = TypeVar("_T")
@ -24,25 +20,25 @@ class PackageMetadata(Protocol):
@overload @overload
def get( def get(
self, name: str, failobj: None = None self, name: str, failobj: None = None
) -> str | None: ... # pragma: no cover ) -> Optional[str]: ... # pragma: no cover
@overload @overload
def get(self, name: str, failobj: _T) -> 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 per python/importlib_metadata#435
@overload @overload
def get_all( def get_all(
self, name: str, failobj: None = None self, name: str, failobj: None = None
) -> list[Any] | None: ... # pragma: no cover ) -> Optional[List[Any]]: ... # pragma: no cover
@overload @overload
def get_all(self, name: str, failobj: _T) -> list[Any] | _T: def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
""" """
Return all values associated with a possibly multi-valued key. Return all values associated with a possibly multi-valued key.
""" """
@property @property
def json(self) -> dict[str, str | list[str]]: def json(self) -> Dict[str, Union[str, List[str]]]:
""" """
A JSON-compatible form of the metadata. A JSON-compatible form of the metadata.
""" """
@ -54,11 +50,11 @@ class SimplePath(Protocol):
""" """
def joinpath( def joinpath(
self, other: str | os.PathLike[str] self, other: Union[str, os.PathLike[str]]
) -> SimplePath: ... # pragma: no cover ) -> SimplePath: ... # pragma: no cover
def __truediv__( def __truediv__(
self, other: str | os.PathLike[str] self, other: Union[str, os.PathLike[str]]
) -> SimplePath: ... # pragma: no cover ) -> SimplePath: ... # pragma: no cover
@property @property

View file

@ -1,15 +0,0 @@
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
"""

View file

@ -1,14 +1,9 @@
# from jaraco.path 3.7.2 # from jaraco.path 3.7
from __future__ import annotations
import functools import functools
import pathlib import pathlib
from collections.abc import Mapping from typing import Dict, Protocol, Union
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable from typing import runtime_checkable
if TYPE_CHECKING:
from typing_extensions import Self
class Symlink(str): class Symlink(str):
@ -17,25 +12,29 @@ class Symlink(str):
""" """
FilesSpec = Mapping[str, Union[str, bytes, Symlink, 'FilesSpec']] FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore
@runtime_checkable @runtime_checkable
class TreeMaker(Protocol): class TreeMaker(Protocol):
def __truediv__(self, other, /) -> Self: ... def __truediv__(self, *args, **kwargs): ... # pragma: no cover
def mkdir(self, *, exist_ok) -> object: ...
def write_text(self, content, /, *, encoding) -> object: ... def mkdir(self, **kwargs): ... # pragma: no cover
def write_bytes(self, content, /) -> object: ...
def symlink_to(self, target, /) -> object: ... 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 _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker: def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore
def build( def build(
spec: FilesSpec, spec: FilesSpec,
prefix: str | TreeMaker = pathlib.Path(), prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore
): ):
""" """
Build a set of files/directories, as described by the spec. Build a set of files/directories, as described by the spec.
@ -67,24 +66,23 @@ def build(
@functools.singledispatch @functools.singledispatch
def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None: def create(content: Union[str, bytes, FilesSpec], path):
path.mkdir(exist_ok=True) path.mkdir(exist_ok=True)
# Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union build(content, prefix=path) # type: ignore
build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727
@create.register @create.register
def _(content: bytes, path: TreeMaker) -> None: def _(content: bytes, path):
path.write_bytes(content) path.write_bytes(content)
@create.register @create.register
def _(content: str, path: TreeMaker) -> None: def _(content: str, path):
path.write_text(content, encoding='utf-8') path.write_text(content, encoding='utf-8')
@create.register @create.register
def _(content: Symlink, path: TreeMaker) -> None: def _(content: Symlink, path):
path.symlink_to(content) path.symlink_to(content)

View file

@ -1,11 +1,11 @@
import contextlib
import copy
import functools
import json
import pathlib
import shutil
import sys import sys
import copy
import json
import shutil
import pathlib
import textwrap import textwrap
import functools
import contextlib
from test.support import import_helper from test.support import import_helper
from test.support import os_helper from test.support import os_helper
@ -14,10 +14,14 @@ from test.support import requires_zlib
from . import _path from . import _path
from ._path import FilesSpec from ._path import FilesSpec
if sys.version_info >= (3, 9):
from importlib import resources try:
else: from importlib import resources # type: ignore
import importlib_resources as resources
getattr(resources, 'files')
getattr(resources, 'as_file')
except (ImportError, AttributeError):
import importlib_resources as resources # type: ignore
@contextlib.contextmanager @contextlib.contextmanager

View file

@ -1,8 +1,9 @@
import importlib
import re import re
import textwrap import textwrap
import unittest import unittest
import importlib
from . import fixtures
from importlib.metadata import ( from importlib.metadata import (
Distribution, Distribution,
PackageNotFoundError, PackageNotFoundError,
@ -14,8 +15,6 @@ from importlib.metadata import (
version, version,
) )
from . import fixtures
class APITests( class APITests(
fixtures.EggInfoPkg, fixtures.EggInfoPkg,

View file

@ -1,7 +1,8 @@
import importlib
import pickle
import re import re
import pickle
import unittest import unittest
import importlib
import importlib.metadata
from test.support import os_helper from test.support import os_helper
try: try:
@ -9,6 +10,8 @@ try:
except ImportError: except ImportError:
from .stubs import fake_filesystem_unittest as ffs from .stubs import fake_filesystem_unittest as ffs
from . import fixtures
from ._path import Symlink
from importlib.metadata import ( from importlib.metadata import (
Distribution, Distribution,
EntryPoint, EntryPoint,
@ -21,9 +24,6 @@ from importlib.metadata import (
version, version,
) )
from . import fixtures
from ._path import Symlink
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
version_pattern = r'\d+\.\d+(\.\d)?' version_pattern = r'\d+\.\d+(\.\d)?'
@ -157,16 +157,6 @@ class InvalidMetadataTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCa
dist = Distribution.from_name('foo') dist = Distribution.from_name('foo')
assert dist.version == "1.0" 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): class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
@staticmethod @staticmethod

View file

@ -1,6 +1,7 @@
import sys import sys
import unittest import unittest
from . import fixtures
from importlib.metadata import ( from importlib.metadata import (
PackageNotFoundError, PackageNotFoundError,
distribution, distribution,
@ -10,8 +11,6 @@ from importlib.metadata import (
version, version,
) )
from . import fixtures
class TestZip(fixtures.ZipFixtures, unittest.TestCase): class TestZip(fixtures.ZipFixtures, unittest.TestCase):
def setUp(self): def setUp(self):

View file

@ -1,6 +0,0 @@
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.