diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index 1e2cea4009482a..b59587e80165e5 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -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 +import os +import re import abc -import collections +import sys +import json 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 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 +import collections 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 ._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__ = [ 'Distribution', @@ -60,7 +53,7 @@ def __str__(self) -> str: return f"No package metadata was found for {self.name}" @property - def name(self) -> str: # type: ignore[override] # make readonly + def name(self) -> str: # type: ignore[override] (name,) = self.args return name @@ -130,12 +123,6 @@ 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. @@ -151,30 +138,6 @@ 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( @@ -202,44 +165,38 @@ class EntryPoint: value: str group: str - dist: Distribution | None = None + dist: Optional[Distribution] = 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. """ - module = import_module(self.module) - attrs = filter(None, (self.attr or '').split('.')) + match = cast(Match, self.pattern.match(self.value)) + module = import_module(match.group('module')) + attrs = filter(None, (match.group('attr') or '').split('.')) return functools.reduce(getattr, attrs, module) @property def module(self) -> str: - return self._match.module + match = self.pattern.match(self.value) + assert match is not None + return match.group('module') @property def attr(self) -> str: - return self._match.attr + match = self.pattern.match(self.value) + assert match is not None + return match.group('attr') @property - def extras(self) -> list[str]: - return re.findall(r'\w+', self._match.extras or '') - - @functools.cached_property - def _match(self) -> _EntryPointMatch: + def extras(self) -> List[str]: match = self.pattern.match(self.value) - 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()) + assert match is not None + return re.findall(r'\w+', match.group('extras') or '') def _for(self, dist): vars(self).update(dist=dist) @@ -265,26 +222,9 @@ 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 @@ -314,7 +254,7 @@ class EntryPoints(tuple): __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. """ @@ -338,14 +278,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. """ @@ -366,11 +306,11 @@ def _from_text(text): class PackagePath(pathlib.PurePosixPath): """A reference to a path in a package""" - hash: FileHash | None + hash: Optional[FileHash] size: int 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) def read_binary(self) -> bytes: @@ -401,7 +341,7 @@ class Distribution(metaclass=abc.ABCMeta): """ @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. Python distribution metadata is organized by blobs of text @@ -428,17 +368,6 @@ 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 @@ -461,7 +390,7 @@ def from_name(cls, name: str) -> Distribution: @classmethod def discover( - cls, *, context: DistributionFinder.Context | None = None, **kwargs + cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs ) -> Iterable[Distribution]: """Return an iterable of Distribution objects for all packages. @@ -507,7 +436,7 @@ def _discover_resolvers(): return filter(None, declared) @property - def metadata(self) -> _meta.PackageMetadata | None: + def metadata(self) -> _meta.PackageMetadata: """Return the parsed metadata for this Distribution. The returned object will have keys that name the various bits of @@ -517,8 +446,10 @@ def metadata(self) -> _meta.PackageMetadata | None: Custom providers may provide the METADATA file or override this property. """ + # deferred for performance (python/cpython#109829) + from . import _adapters - text = ( + opt_text = ( self.read_text('METADATA') or self.read_text('PKG-INFO') # This last clause is here to support old egg-info files. Its @@ -526,20 +457,13 @@ def metadata(self) -> _meta.PackageMetadata | None: # (which points to the egg-info file) attribute unchanged. or self.read_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 - + text = cast(str, opt_text) return _adapters.Message(email.message_from_string(text)) @property def name(self) -> str: """Return the 'Name' metadata for the distribution package.""" - return md_none(self.metadata)['Name'] + return self.metadata['Name'] @property def _normalized_name(self): @@ -549,7 +473,7 @@ def _normalized_name(self): @property def version(self) -> str: """Return the 'Version' metadata for the distribution package.""" - return md_none(self.metadata)['Version'] + return self.metadata['Version'] @property def entry_points(self) -> EntryPoints: @@ -562,7 +486,7 @@ def entry_points(self) -> EntryPoints: return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) @property - def files(self) -> list[PackagePath] | None: + def files(self) -> Optional[List[PackagePath]]: """Files in this distribution. :return: List of PackagePath for this distribution or None @@ -655,7 +579,7 @@ def _read_files_egginfo_sources(self): return text and map('"{}"'.format, text.splitlines()) @property - def requires(self) -> list[str] | None: + 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) @@ -711,9 +635,6 @@ 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), @@ -761,7 +682,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. @@ -798,7 +719,7 @@ class FastPath: True """ - @functools.lru_cache() # type: ignore[misc] + @functools.lru_cache() # type: ignore def __new__(cls, root): return super().__new__(cls) @@ -816,9 +737,6 @@ 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 @@ -913,7 +831,7 @@ class Prepared: normalized = None legacy_normalized = None - def __init__(self, name: str | None): + def __init__(self, name: Optional[str]): self.name = name if name is None: return @@ -976,7 +894,7 @@ def __init__(self, path: SimplePath) -> None: """ 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( FileNotFoundError, IsADirectoryError, @@ -1040,7 +958,7 @@ def distributions(**kwargs) -> Iterable[Distribution]: 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. :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) -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. :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 -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. @@ -1102,7 +1020,7 @@ def requires(distribution_name: str) -> list[str] | None: 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. @@ -1115,7 +1033,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(md_none(dist.metadata)['Name']) + pkg_to_dist[pkg].append(dist.metadata['Name']) return dict(pkg_to_dist) @@ -1123,7 +1041,7 @@ def _top_level_declared(dist): 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. """ @@ -1149,10 +1067,11 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - # Defer import of inspect for performance (python/cpython#118761) - import inspect - - return _topmost(name) or inspect.getmodulename(name) or str(name) + return _topmost(name) or ( + # python/typeshed#10328 + inspect.getmodulename(name) # type: ignore + or str(name) + ) def _top_level_inferred(dist): diff --git a/Lib/importlib/metadata/_adapters.py b/Lib/importlib/metadata/_adapters.py index f5b30dd92cde69..6223263ed53f22 100644 --- a/Lib/importlib/metadata/_adapters.py +++ b/Lib/importlib/metadata/_adapters.py @@ -1,58 +1,11 @@ -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 - ... - ... First line of description. - ... Second line of description. - ... - ... Fourth line! - ... ''').lstrip().replace('', '') - >>> 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. - - Fourth line! - - - """ - multiple_use_keys = set( map( FoldedCase, @@ -104,20 +57,15 @@ def __getitem__(self, item): def _repair_headers(self): def redent(value): "Correct for RFC822 indentation" - indent = ' ' * 8 - if not value or '\n' + indent not in value: + if not value or '\n' not in value: return value - return textwrap.dedent(indent + value) + return textwrap.dedent(' ' * 8 + 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): """ diff --git a/Lib/importlib/metadata/_collections.py b/Lib/importlib/metadata/_collections.py index fc5045d36be572..cf0954e1a30546 100644 --- a/Lib/importlib/metadata/_collections.py +++ b/Lib/importlib/metadata/_collections.py @@ -1,5 +1,4 @@ import collections -import typing # from jaraco.collections 3.3 @@ -25,10 +24,7 @@ def freeze(self): self._frozen = lambda key: self.default_factory() -class Pair(typing.NamedTuple): - name: str - value: str - +class Pair(collections.namedtuple('Pair', 'name value')): @classmethod def parse(cls, text): return cls(*map(str.strip, text.split("=", 1))) diff --git a/Lib/importlib/metadata/_functools.py b/Lib/importlib/metadata/_functools.py index 5dda6a2199ad0b..71f66bd03cb713 100644 --- a/Lib/importlib/metadata/_functools.py +++ b/Lib/importlib/metadata/_functools.py @@ -1,5 +1,5 @@ -import functools import types +import functools # from jaraco.functools 3.3 diff --git a/Lib/importlib/metadata/_meta.py b/Lib/importlib/metadata/_meta.py index 0c20eff3da7522..1927d0f624d82f 100644 --- a/Lib/importlib/metadata/_meta.py +++ b/Lib/importlib/metadata/_meta.py @@ -1,13 +1,9 @@ from __future__ import annotations import os -from collections.abc import Iterator -from typing import ( - Any, - Protocol, - TypeVar, - overload, -) +from typing import Protocol +from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload + _T = TypeVar("_T") @@ -24,25 +20,25 @@ def __iter__(self) -> Iterator[str]: ... # pragma: no cover @overload def get( self, name: str, failobj: None = None - ) -> str | None: ... # pragma: no cover + ) -> Optional[str]: ... # pragma: no cover @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 def get_all( self, name: str, failobj: None = None - ) -> list[Any] | None: ... # pragma: no cover + ) -> Optional[List[Any]]: ... # pragma: no cover @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. """ @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. """ @@ -54,11 +50,11 @@ class SimplePath(Protocol): """ def joinpath( - self, other: str | os.PathLike[str] + self, other: Union[str, os.PathLike[str]] ) -> SimplePath: ... # pragma: no cover def __truediv__( - self, other: str | os.PathLike[str] + self, other: Union[str, os.PathLike[str]] ) -> SimplePath: ... # pragma: no cover @property diff --git a/Lib/importlib/metadata/_typing.py b/Lib/importlib/metadata/_typing.py deleted file mode 100644 index 32b1d2b98ac987..00000000000000 --- a/Lib/importlib/metadata/_typing.py +++ /dev/null @@ -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 -""" diff --git a/Lib/test/test_importlib/metadata/_path.py b/Lib/test/test_importlib/metadata/_path.py index e63d889f96bf13..b3cfb9cd549d6c 100644 --- a/Lib/test/test_importlib/metadata/_path.py +++ b/Lib/test/test_importlib/metadata/_path.py @@ -1,14 +1,9 @@ -# from jaraco.path 3.7.2 - -from __future__ import annotations +# from jaraco.path 3.7 import functools import pathlib -from collections.abc import Mapping -from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable - -if TYPE_CHECKING: - from typing_extensions import Self +from typing import Dict, Protocol, Union +from typing import runtime_checkable 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 class TreeMaker(Protocol): - 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 __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 _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker: - return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) +def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore def build( 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. @@ -67,24 +66,23 @@ def build( @functools.singledispatch -def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None: +def create(content: Union[str, bytes, FilesSpec], path): 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[arg-type] # python/mypy#11727 + build(content, prefix=path) # type: ignore @create.register -def _(content: bytes, path: TreeMaker) -> None: +def _(content: bytes, path): path.write_bytes(content) @create.register -def _(content: str, path: TreeMaker) -> None: +def _(content: str, path): path.write_text(content, encoding='utf-8') @create.register -def _(content: Symlink, path: TreeMaker) -> None: +def _(content: Symlink, path): path.symlink_to(content) diff --git a/Lib/test/test_importlib/metadata/fixtures.py b/Lib/test/test_importlib/metadata/fixtures.py index 494047dc98f9b6..826b1b3259b4cd 100644 --- a/Lib/test/test_importlib/metadata/fixtures.py +++ b/Lib/test/test_importlib/metadata/fixtures.py @@ -1,11 +1,11 @@ -import contextlib +import sys import copy -import functools import json -import pathlib import shutil -import sys +import pathlib import textwrap +import functools +import contextlib from test.support import import_helper from test.support import os_helper @@ -14,10 +14,14 @@ from . import _path from ._path import FilesSpec -if sys.version_info >= (3, 9): - from importlib import resources -else: - import importlib_resources as resources + +try: + from importlib import resources # type: ignore + + getattr(resources, 'files') + getattr(resources, 'as_file') +except (ImportError, AttributeError): + import importlib_resources as resources # type: ignore @contextlib.contextmanager diff --git a/Lib/test/test_importlib/metadata/test_api.py b/Lib/test/test_importlib/metadata/test_api.py index 9f6e12c87e859c..813febf269593b 100644 --- a/Lib/test/test_importlib/metadata/test_api.py +++ b/Lib/test/test_importlib/metadata/test_api.py @@ -1,8 +1,9 @@ -import importlib import re import textwrap import unittest +import importlib +from . import fixtures from importlib.metadata import ( Distribution, PackageNotFoundError, @@ -14,8 +15,6 @@ version, ) -from . import fixtures - class APITests( fixtures.EggInfoPkg, diff --git a/Lib/test/test_importlib/metadata/test_main.py b/Lib/test/test_importlib/metadata/test_main.py index 83b686babfdb7a..a0bc8222d5ba24 100644 --- a/Lib/test/test_importlib/metadata/test_main.py +++ b/Lib/test/test_importlib/metadata/test_main.py @@ -1,7 +1,8 @@ -import importlib -import pickle import re +import pickle import unittest +import importlib +import importlib.metadata from test.support import os_helper try: @@ -9,6 +10,8 @@ except ImportError: from .stubs import fake_filesystem_unittest as ffs +from . import fixtures +from ._path import Symlink from importlib.metadata import ( Distribution, EntryPoint, @@ -21,9 +24,6 @@ version, ) -from . import fixtures -from ._path import Symlink - class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): version_pattern = r'\d+\.\d+(\.\d)?' @@ -157,16 +157,6 @@ 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 diff --git a/Lib/test/test_importlib/metadata/test_zip.py b/Lib/test/test_importlib/metadata/test_zip.py index fcb649f3736076..276f6288c91598 100644 --- a/Lib/test/test_importlib/metadata/test_zip.py +++ b/Lib/test/test_importlib/metadata/test_zip.py @@ -1,6 +1,7 @@ import sys import unittest +from . import fixtures from importlib.metadata import ( PackageNotFoundError, distribution, @@ -10,8 +11,6 @@ version, ) -from . import fixtures - class TestZip(fixtures.ZipFixtures, unittest.TestCase): def setUp(self): diff --git a/Misc/NEWS.d/next/Library/2025-08-17-10-22-31.gh-issue-132947.XR4MJ8.rst b/Misc/NEWS.d/next/Library/2025-08-17-10-22-31.gh-issue-132947.XR4MJ8.rst deleted file mode 100644 index 8a2b0a4981eae9..00000000000000 --- a/Misc/NEWS.d/next/Library/2025-08-17-10-22-31.gh-issue-132947.XR4MJ8.rst +++ /dev/null @@ -1,6 +0,0 @@ -Applied changes to ``importlib.metadata`` from `importlib_metadata 8.7 -`_, -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.