1import email.message
2import email.policy
3import re
4import textwrap
5
6from ._text import FoldedCase
7
8
9class RawPolicy(email.policy.EmailPolicy):
10 def fold(self, name, value):
11 folded = self.linesep.join(
12 textwrap
13 .indent(value, prefix=' ' * 8, predicate=lambda line: True)
14 .lstrip()
15 .splitlines()
16 )
17 return f'{name}: {folded}{self.linesep}'
18
19
20class Message(email.message.Message):
21 r"""
22 Specialized Message subclass to handle metadata naturally.
23
24 Reads values that may have newlines in them and converts the
25 payload to the Description.
26
27 >>> msg_text = textwrap.dedent('''
28 ... Name: Foo
29 ... Version: 3.0
30 ... License: blah
31 ... de-blah
32 ... <BLANKLINE>
33 ... First line of description.
34 ... Second line of description.
35 ... <BLANKLINE>
36 ... Fourth line!
37 ... ''').lstrip().replace('<BLANKLINE>', '')
38 >>> msg = Message(email.message_from_string(msg_text))
39 >>> msg['Description']
40 'First line of description.\nSecond line of description.\n\nFourth line!\n'
41
42 Message should render even if values contain newlines.
43
44 >>> print(msg)
45 Name: Foo
46 Version: 3.0
47 License: blah
48 de-blah
49 Description: First line of description.
50 Second line of description.
51 <BLANKLINE>
52 Fourth line!
53 <BLANKLINE>
54 <BLANKLINE>
55 """
56
57 multiple_use_keys = frozenset(
58 map(
59 FoldedCase,
60 [
61 'Classifier',
62 'Import-Name',
63 'Import-Namespace',
64 'License-File',
65 'Obsoletes-Dist',
66 'Platform',
67 'Project-URL',
68 'Provides-Dist',
69 'Provides-Extra',
70 'Requires-Dist',
71 'Requires-External',
72 'Supported-Platform',
73 'Dynamic',
74 ],
75 )
76 )
77 """
78 Keys that may be indicated multiple times in Core Metadata.
79 """
80
81 def __new__(cls, orig: email.message.Message):
82 res = super().__new__(cls)
83 vars(res).update(vars(orig))
84 return res
85
86 def __init__(self, *args, **kwargs):
87 self._headers = self._repair_headers()
88
89 # suppress spurious error from mypy
90 def __iter__(self):
91 return super().__iter__()
92
93 def __getitem__(self, item):
94 """
95 Override parent behavior to typical dict behavior.
96
97 ``email.message.Message`` will emit None values for missing
98 keys. Typical mappings, including this ``Message``, will raise
99 a key error for missing keys.
100
101 Ref python/importlib_metadata#371.
102 """
103 res = super().__getitem__(item)
104 if res is None:
105 raise KeyError(item)
106 return res
107
108 def _repair_headers(self):
109 def redent(value):
110 "Correct for RFC822 indentation"
111 indent = ' ' * 8
112 if not value or '\n' + indent not in value:
113 return value
114 return textwrap.dedent(indent + value)
115
116 headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
117 if self._payload:
118 headers.append(('Description', self.get_payload()))
119 self.set_payload('')
120 return headers
121
122 def as_string(self):
123 return super().as_string(policy=RawPolicy())
124
125 @property
126 def json(self):
127 """
128 Convert PackageMetadata to a JSON-compatible format
129 per PEP 0566.
130 """
131
132 def transform(key):
133 value = self.get_all(key) if key in self.multiple_use_keys else self[key]
134 if key == 'Keywords':
135 value = re.split(r'\s+', value)
136 tk = key.lower().replace('-', '_')
137 return tk, value
138
139 return dict(map(transform, map(FoldedCase, self)))