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