| | 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 |
| | ... <BLANKLINE> |
| | ... First line of description. |
| | ... Second line of description. |
| | ... <BLANKLINE> |
| | ... Fourth line! |
| | ... ''').lstrip().replace('<BLANKLINE>', '') |
| | >>> 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. |
| | <BLANKLINE> |
| | Fourth line! |
| | <BLANKLINE> |
| | <BLANKLINE> |
| | """ |
| |
|
| | 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() |
| |
|
| | |
| | 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))) |
| |
|