codekingpro/portable-devtools
114k
1import email.message2import email.policy3import re4import textwrap5 6from ._text import FoldedCase7 8 9class RawPolicy(email.policy.EmailPolicy):10 def fold(self, name, value):11 folded = self.linesep.join(12 textwrap13 .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 the25 payload to the Description.26 27 >>> msg_text = textwrap.dedent('''28 ... Name: Foo29 ... Version: 3.030 ... License: blah31 ... de-blah32 ... <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: Foo46 Version: 3.047 License: blah48 de-blah49 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 res82 83 def __init__(self, *args, **kwargs):84 self._headers = self._repair_headers()85 86 # suppress spurious error from mypy87 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 missing95 keys. Typical mappings, including this ``Message``, will raise96 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 res104 105 def _repair_headers(self):106 def redent(value):107 "Correct for RFC822 indentation"108 indent = ' ' * 8109 if not value or '\n' + indent not in value:110 return value111 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 headers118 119 def as_string(self):120 return super().as_string(policy=RawPolicy())121 122 @property123 def json(self):124 """125 Convert PackageMetadata to a JSON-compatible format126 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, value135 136 return dict(map(transform, map(FoldedCase, self)))137 