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 = set(
58 map(
59 FoldedCase,
60 [
61 'Classifier',
62 'Obsoletes-Dist',
63 'Platform',
64 'Project-URL',
65 'Provides-Dist',
66 'Provides-Extra',
67 'Requires-Dist',
68 'Requires-External',
69 'Supported-Platform',
70 'Dynamic',
71 ],
72 )
73 )
74 """
75 Keys that may be indicated multiple times per PEP 566.
76 """
77
78 def __new__(cls, orig: email.message.Message):
79 res = super().__new__(cls)
80 vars(res).update(vars(orig))
81 return res
82
83 def __init__(self, *args, **kwargs):
84 self._headers = self._repair_headers()
85
86 # suppress spurious error from mypy
87 def __iter__(self):
88 return super().__iter__()
89
90 def __getitem__(self, item):
91 """
92 Override parent behavior to typical dict behavior.
93
94 ``email.message.Message`` will emit None values for missing
95 keys. Typical mappings, including this ``Message``, will raise
96 a key error for missing keys.
97
98 Ref python/importlib_metadata#371.
99 """
100 res = super().__getitem__(item)
101 if res is None:
102 raise KeyError(item)
103 return res
104
105 def _repair_headers(self):
106 def redent(value):
107 "Correct for RFC822 indentation"
108 indent = ' ' * 8
109 if not value or '\n' + indent not in value:
110 return value
111 return textwrap.dedent(indent + value)
112
113 headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
114 if self._payload:
115 headers.append(('Description', self.get_payload()))
116 self.set_payload('')
117 return headers
118
119 def as_string(self):
120 return super().as_string(policy=RawPolicy())
121
122 @property
123 def json(self):
124 """
125 Convert PackageMetadata to a JSON-compatible format
126 per PEP 0566.
127 """
128
129 def transform(key):
130 value = self.get_all(key) if key in self.multiple_use_keys else self[key]
131 if key == 'Keywords':
132 value = re.split(r'\s+', value)
133 tk = key.lower().replace('-', '_')
134 return tk, value
135
136 return dict(map(transform, map(FoldedCase, self)))