• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import functools
2import warnings
3import re
4import textwrap
5import email.message
6
7from ._text import FoldedCase
8
9
10# Do not remove prior to 2024-01-01 or Python 3.14
11_warn = functools.partial(
12    warnings.warn,
13    "Implicit None on return values is deprecated and will raise KeyErrors.",
14    DeprecationWarning,
15    stacklevel=2,
16)
17
18
19class Message(email.message.Message):
20    multiple_use_keys = set(
21        map(
22            FoldedCase,
23            [
24                'Classifier',
25                'Obsoletes-Dist',
26                'Platform',
27                'Project-URL',
28                'Provides-Dist',
29                'Provides-Extra',
30                'Requires-Dist',
31                'Requires-External',
32                'Supported-Platform',
33                'Dynamic',
34            ],
35        )
36    )
37    """
38    Keys that may be indicated multiple times per PEP 566.
39    """
40
41    def __new__(cls, orig: email.message.Message):
42        res = super().__new__(cls)
43        vars(res).update(vars(orig))
44        return res
45
46    def __init__(self, *args, **kwargs):
47        self._headers = self._repair_headers()
48
49    # suppress spurious error from mypy
50    def __iter__(self):
51        return super().__iter__()
52
53    def __getitem__(self, item):
54        """
55        Warn users that a ``KeyError`` can be expected when a
56        missing key is supplied. Ref python/importlib_metadata#371.
57        """
58        res = super().__getitem__(item)
59        if res is None:
60            _warn()
61        return res
62
63    def _repair_headers(self):
64        def redent(value):
65            "Correct for RFC822 indentation"
66            if not value or '\n' not in value:
67                return value
68            return textwrap.dedent(' ' * 8 + value)
69
70        headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
71        if self._payload:
72            headers.append(('Description', self.get_payload()))
73        return headers
74
75    @property
76    def json(self):
77        """
78        Convert PackageMetadata to a JSON-compatible format
79        per PEP 0566.
80        """
81
82        def transform(key):
83            value = self.get_all(key) if key in self.multiple_use_keys else self[key]
84            if key == 'Keywords':
85                value = re.split(r'\s+', value)
86            tk = key.lower().replace('-', '_')
87            return tk, value
88
89        return dict(map(transform, map(FoldedCase, self)))
90