Skip to content

gh-132947: Apply changes from importlib_metadata 8.7 #137885

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 143 additions & 62 deletions Lib/importlib/metadata/__init__.py

Large diffs are not rendered by default.

58 changes: 55 additions & 3 deletions Lib/importlib/metadata/_adapters.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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):
"""
Expand Down
6 changes: 5 additions & 1 deletion Lib/importlib/metadata/_collections.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import collections
import typing


# from jaraco.collections 3.3
Expand All @@ -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)))
2 changes: 1 addition & 1 deletion Lib/importlib/metadata/_functools.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import types
import functools
import types


# from jaraco.functools 3.3
Expand Down
24 changes: 14 additions & 10 deletions Lib/importlib/metadata/_meta.py
Original file line number Diff line number Diff line change
@@ -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")

Expand All @@ -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.
"""
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions Lib/importlib/metadata/_typing.py
Original file line number Diff line number Diff line change
@@ -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
"""
44 changes: 23 additions & 21 deletions Lib/test/test_importlib/metadata/_path.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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.
Expand Down Expand Up @@ -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)


Expand Down
20 changes: 8 additions & 12 deletions Lib/test/test_importlib/metadata/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import sys
import contextlib
import copy
import functools
import json
import shutil
import pathlib
import shutil
import sys
import textwrap
import functools
import contextlib

from test.support import import_helper
from test.support import os_helper
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions Lib/test/test_importlib/metadata/test_api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import importlib
import re
import textwrap
import unittest
import importlib

from . import fixtures
from importlib.metadata import (
Distribution,
PackageNotFoundError,
Expand All @@ -15,6 +14,8 @@
version,
)

from . import fixtures


class APITests(
fixtures.EggInfoPkg,
Expand Down
20 changes: 15 additions & 5 deletions Lib/test/test_importlib/metadata/test_main.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import re
import importlib
import pickle
import re
import unittest
import importlib
import importlib.metadata
from test.support import os_helper

try:
import pyfakefs.fake_filesystem_unittest as ffs
except ImportError:
from .stubs import fake_filesystem_unittest as ffs

from . import fixtures
from ._path import Symlink
from importlib.metadata import (
Distribution,
EntryPoint,
Expand All @@ -24,6 +21,9 @@
version,
)

from . import fixtures
from ._path import Symlink


class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
version_pattern = r'\d+\.\d+(\.\d)?'
Expand Down Expand Up @@ -157,6 +157,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
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_importlib/metadata/test_zip.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import sys
import unittest

from . import fixtures
from importlib.metadata import (
PackageNotFoundError,
distribution,
Expand All @@ -11,6 +10,8 @@
version,
)

from . import fixtures


class TestZip(fixtures.ZipFixtures, unittest.TestCase):
def setUp(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -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