Skip to content

Commit b67ac80

Browse files
committed
Merge tag 'v8.7.0' into cpython
2 parents 55c9652 + 708dff4 commit b67ac80

File tree

11 files changed

+284
-118
lines changed

11 files changed

+284
-118
lines changed

Lib/importlib/metadata/__init__.py

Lines changed: 143 additions & 62 deletions
Large diffs are not rendered by default.

Lib/importlib/metadata/_adapters.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,58 @@
1+
import email.message
2+
import email.policy
13
import re
24
import textwrap
3-
import email.message
45

56
from ._text import FoldedCase
67

78

9+
class RawPolicy(email.policy.EmailPolicy):
10+
def fold(self, name, value):
11+
folded = self.linesep.join(
12+
textwrap.indent(value, prefix=' ' * 8, predicate=lambda line: True)
13+
.lstrip()
14+
.splitlines()
15+
)
16+
return f'{name}: {folded}{self.linesep}'
17+
18+
819
class Message(email.message.Message):
20+
r"""
21+
Specialized Message subclass to handle metadata naturally.
22+
23+
Reads values that may have newlines in them and converts the
24+
payload to the Description.
25+
26+
>>> msg_text = textwrap.dedent('''
27+
... Name: Foo
28+
... Version: 3.0
29+
... License: blah
30+
... de-blah
31+
... <BLANKLINE>
32+
... First line of description.
33+
... Second line of description.
34+
... <BLANKLINE>
35+
... Fourth line!
36+
... ''').lstrip().replace('<BLANKLINE>', '')
37+
>>> msg = Message(email.message_from_string(msg_text))
38+
>>> msg['Description']
39+
'First line of description.\nSecond line of description.\n\nFourth line!\n'
40+
41+
Message should render even if values contain newlines.
42+
43+
>>> print(msg)
44+
Name: Foo
45+
Version: 3.0
46+
License: blah
47+
de-blah
48+
Description: First line of description.
49+
Second line of description.
50+
<BLANKLINE>
51+
Fourth line!
52+
<BLANKLINE>
53+
<BLANKLINE>
54+
"""
55+
956
multiple_use_keys = set(
1057
map(
1158
FoldedCase,
@@ -57,15 +104,20 @@ def __getitem__(self, item):
57104
def _repair_headers(self):
58105
def redent(value):
59106
"Correct for RFC822 indentation"
60-
if not value or '\n' not in value:
107+
indent = ' ' * 8
108+
if not value or '\n' + indent not in value:
61109
return value
62-
return textwrap.dedent(' ' * 8 + value)
110+
return textwrap.dedent(indent + value)
63111

64112
headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
65113
if self._payload:
66114
headers.append(('Description', self.get_payload()))
115+
self.set_payload('')
67116
return headers
68117

118+
def as_string(self):
119+
return super().as_string(policy=RawPolicy())
120+
69121
@property
70122
def json(self):
71123
"""

Lib/importlib/metadata/_collections.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import collections
2+
import typing
23

34

45
# from jaraco.collections 3.3
@@ -24,7 +25,10 @@ def freeze(self):
2425
self._frozen = lambda key: self.default_factory()
2526

2627

27-
class Pair(collections.namedtuple('Pair', 'name value')):
28+
class Pair(typing.NamedTuple):
29+
name: str
30+
value: str
31+
2832
@classmethod
2933
def parse(cls, text):
3034
return cls(*map(str.strip, text.split("=", 1)))

Lib/importlib/metadata/_functools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import types
21
import functools
2+
import types
33

44

55
# from jaraco.functools 3.3

Lib/importlib/metadata/_meta.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import annotations
22

33
import os
4-
from typing import Protocol
5-
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
6-
4+
from collections.abc import Iterator
5+
from typing import (
6+
Any,
7+
Protocol,
8+
TypeVar,
9+
overload,
10+
)
711

812
_T = TypeVar("_T")
913

@@ -20,25 +24,25 @@ def __iter__(self) -> Iterator[str]: ... # pragma: no cover
2024
@overload
2125
def get(
2226
self, name: str, failobj: None = None
23-
) -> Optional[str]: ... # pragma: no cover
27+
) -> str | None: ... # pragma: no cover
2428

2529
@overload
26-
def get(self, name: str, failobj: _T) -> Union[str, _T]: ... # pragma: no cover
30+
def get(self, name: str, failobj: _T) -> str | _T: ... # pragma: no cover
2731

2832
# overload per python/importlib_metadata#435
2933
@overload
3034
def get_all(
3135
self, name: str, failobj: None = None
32-
) -> Optional[List[Any]]: ... # pragma: no cover
36+
) -> list[Any] | None: ... # pragma: no cover
3337

3438
@overload
35-
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
39+
def get_all(self, name: str, failobj: _T) -> list[Any] | _T:
3640
"""
3741
Return all values associated with a possibly multi-valued key.
3842
"""
3943

4044
@property
41-
def json(self) -> Dict[str, Union[str, List[str]]]:
45+
def json(self) -> dict[str, str | list[str]]:
4246
"""
4347
A JSON-compatible form of the metadata.
4448
"""
@@ -50,11 +54,11 @@ class SimplePath(Protocol):
5054
"""
5155

5256
def joinpath(
53-
self, other: Union[str, os.PathLike[str]]
57+
self, other: str | os.PathLike[str]
5458
) -> SimplePath: ... # pragma: no cover
5559

5660
def __truediv__(
57-
self, other: Union[str, os.PathLike[str]]
61+
self, other: str | os.PathLike[str]
5862
) -> SimplePath: ... # pragma: no cover
5963

6064
@property

Lib/importlib/metadata/_typing.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import functools
2+
import typing
3+
4+
from ._meta import PackageMetadata
5+
6+
md_none = functools.partial(typing.cast, PackageMetadata)
7+
"""
8+
Suppress type errors for optional metadata.
9+
10+
Although Distribution.metadata can return None when metadata is corrupt
11+
and thus None, allow callers to assume it's not None and crash if
12+
that's the case.
13+
14+
# python/importlib_metadata#493
15+
"""

Lib/test/test_importlib/metadata/_path.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
# from jaraco.path 3.7
1+
# from jaraco.path 3.7.2
2+
3+
from __future__ import annotations
24

35
import functools
46
import pathlib
5-
from typing import Dict, Protocol, Union
6-
from typing import runtime_checkable
7+
from collections.abc import Mapping
8+
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable
9+
10+
if TYPE_CHECKING:
11+
from typing_extensions import Self
712

813

914
class Symlink(str):
@@ -12,29 +17,25 @@ class Symlink(str):
1217
"""
1318

1419

15-
FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore
20+
FilesSpec = Mapping[str, Union[str, bytes, Symlink, 'FilesSpec']]
1621

1722

1823
@runtime_checkable
1924
class TreeMaker(Protocol):
20-
def __truediv__(self, *args, **kwargs): ... # pragma: no cover
21-
22-
def mkdir(self, **kwargs): ... # pragma: no cover
23-
24-
def write_text(self, content, **kwargs): ... # pragma: no cover
25-
26-
def write_bytes(self, content): ... # pragma: no cover
27-
28-
def symlink_to(self, target): ... # pragma: no cover
25+
def __truediv__(self, other, /) -> Self: ...
26+
def mkdir(self, *, exist_ok) -> object: ...
27+
def write_text(self, content, /, *, encoding) -> object: ...
28+
def write_bytes(self, content, /) -> object: ...
29+
def symlink_to(self, target, /) -> object: ...
2930

3031

31-
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
32-
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore
32+
def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker:
33+
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj)
3334

3435

3536
def build(
3637
spec: FilesSpec,
37-
prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore
38+
prefix: str | TreeMaker = pathlib.Path(),
3839
):
3940
"""
4041
Build a set of files/directories, as described by the spec.
@@ -66,23 +67,24 @@ def build(
6667

6768

6869
@functools.singledispatch
69-
def create(content: Union[str, bytes, FilesSpec], path):
70+
def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None:
7071
path.mkdir(exist_ok=True)
71-
build(content, prefix=path) # type: ignore
72+
# Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union
73+
build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727
7274

7375

7476
@create.register
75-
def _(content: bytes, path):
77+
def _(content: bytes, path: TreeMaker) -> None:
7678
path.write_bytes(content)
7779

7880

7981
@create.register
80-
def _(content: str, path):
82+
def _(content: str, path: TreeMaker) -> None:
8183
path.write_text(content, encoding='utf-8')
8284

8385

8486
@create.register
85-
def _(content: Symlink, path):
87+
def _(content: Symlink, path: TreeMaker) -> None:
8688
path.symlink_to(content)
8789

8890

Lib/test/test_importlib/metadata/fixtures.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import sys
1+
import contextlib
22
import copy
3+
import functools
34
import json
4-
import shutil
55
import pathlib
6+
import shutil
7+
import sys
68
import textwrap
7-
import functools
8-
import contextlib
99

1010
from test.support import import_helper
1111
from test.support import os_helper
@@ -14,14 +14,10 @@
1414
from . import _path
1515
from ._path import FilesSpec
1616

17-
18-
try:
19-
from importlib import resources # type: ignore
20-
21-
getattr(resources, 'files')
22-
getattr(resources, 'as_file')
23-
except (ImportError, AttributeError):
24-
import importlib_resources as resources # type: ignore
17+
if sys.version_info >= (3, 9):
18+
from importlib import resources
19+
else:
20+
import importlib_resources as resources
2521

2622

2723
@contextlib.contextmanager

Lib/test/test_importlib/metadata/test_api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1+
import importlib
12
import re
23
import textwrap
34
import unittest
4-
import importlib
55

6-
from . import fixtures
76
from importlib.metadata import (
87
Distribution,
98
PackageNotFoundError,
@@ -15,6 +14,8 @@
1514
version,
1615
)
1716

17+
from . import fixtures
18+
1819

1920
class APITests(
2021
fixtures.EggInfoPkg,

Lib/test/test_importlib/metadata/test_main.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import re
1+
import importlib
22
import pickle
3+
import re
34
import unittest
4-
import importlib
5-
import importlib.metadata
65
from test.support import os_helper
76

87
try:
98
import pyfakefs.fake_filesystem_unittest as ffs
109
except ImportError:
1110
from .stubs import fake_filesystem_unittest as ffs
1211

13-
from . import fixtures
14-
from ._path import Symlink
1512
from importlib.metadata import (
1613
Distribution,
1714
EntryPoint,
@@ -24,6 +21,9 @@
2421
version,
2522
)
2623

24+
from . import fixtures
25+
from ._path import Symlink
26+
2727

2828
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
2929
version_pattern = r'\d+\.\d+(\.\d)?'
@@ -157,6 +157,16 @@ def test_valid_dists_preferred(self):
157157
dist = Distribution.from_name('foo')
158158
assert dist.version == "1.0"
159159

160+
def test_missing_metadata(self):
161+
"""
162+
Dists with a missing metadata file should return None.
163+
164+
Ref python/importlib_metadata#493.
165+
"""
166+
fixtures.build_files(self.make_pkg('foo-4.3', files={}), self.site_dir)
167+
assert Distribution.from_name('foo').metadata is None
168+
assert metadata('foo') is None
169+
160170

161171
class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
162172
@staticmethod

0 commit comments

Comments
 (0)