import email.message import email.policy import re import textwrap from ._text import FoldedCase class RawPolicy(email.policy.EmailPolicy): def fold(self, name, value): folded = self.linesep.join( textwrap.indent(value, prefix=' ' * 8, predicate=lambda line: True) .lstrip() .splitlines() ) return f'{name}: {folded}{self.linesep}' class Message(email.message.Message): r""" Specialized Message subclass to handle metadata naturally. Reads values that may have newlines in them and converts the payload to the Description. >>> msg_text = textwrap.dedent(''' ... Name: Foo ... Version: 3.0 ... License: blah ... de-blah ... ... First line of description. ... Second line of description. ... ... Fourth line! ... ''').lstrip().replace('', '') >>> msg = Message(email.message_from_string(msg_text)) >>> msg['Description'] 'First line of description.\nSecond line of description.\n\nFourth line!\n' Message should render even if values contain newlines. >>> print(msg) Name: Foo Version: 3.0 License: blah de-blah Description: First line of description. Second line of description. Fourth line! """ multiple_use_keys = set( map( FoldedCase, [ 'Classifier', 'Obsoletes-Dist', 'Platform', 'Project-URL', 'Provides-Dist', 'Provides-Extra', 'Requires-Dist', 'Requires-External', 'Supported-Platform', 'Dynamic', ], ) ) """ Keys that may be indicated multiple times per PEP 566. """ def __new__(cls, orig: email.message.Message): res = super().__new__(cls) vars(res).update(vars(orig)) return res def __init__(self, *args, **kwargs): self._headers = self._repair_headers() # suppress spurious error from mypy def __iter__(self): return super().__iter__() def __getitem__(self, item): """ Override parent behavior to typical dict behavior. ``email.message.Message`` will emit None values for missing keys. Typical mappings, including this ``Message``, will raise a key error for missing keys. Ref python/importlib_metadata#371. """ res = super().__getitem__(item) if res is None: raise KeyError(item) return res def _repair_headers(self): def redent(value): "Correct for RFC822 indentation" indent = ' ' * 8 if not value or '\n' + indent not in value: return value return textwrap.dedent(indent + value) headers = [(key, redent(value)) for key, value in vars(self)['_headers']] if self._payload: headers.append(('Description', self.get_payload())) self.set_payload('') return headers def as_string(self): return super().as_string(policy=RawPolicy()) @property def json(self): """ Convert PackageMetadata to a JSON-compatible format per PEP 0566. """ def transform(key): value = self.get_all(key) if key in self.multiple_use_keys else self[key] if key == 'Keywords': value = re.split(r'\s+', value) tk = key.lower().replace('-', '_') return tk, value return dict(map(transform, map(FoldedCase, self)))