From 319a5bd8189e5e56377a2c5d2b8d504aeae3cfbf Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 13 Sep 2024 13:53:04 -0400 Subject: [PATCH] fix: add unicode and join multilines (#150) * fix: add unicode and join multilines Signed-off-by: Henry Schreiner * fix: custom email policy Signed-off-by: Henry Schreiner * chore: reduce diff Signed-off-by: Henry Schreiner * fix: typing Signed-off-by: Henry Schreiner * refactor: EmailMessage -> Message Signed-off-by: Henry Schreiner --------- Signed-off-by: Henry Schreiner --- pyproject_metadata/__init__.py | 35 ++++++++++++------------------- tests/test_rfc822.py | 38 ++++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/pyproject_metadata/__init__.py b/pyproject_metadata/__init__.py index a5778d8..01d4bb2 100644 --- a/pyproject_metadata/__init__.py +++ b/pyproject_metadata/__init__.py @@ -66,7 +66,6 @@ __all__ = [ 'ConfigurationError', 'ConfigurationWarning', - 'RFC822Message', 'License', 'Readme', 'StandardMetadata', @@ -120,35 +119,29 @@ class ConfigurationWarning(UserWarning): @dataclasses.dataclass class _SmartMessageSetter: """ - This provides a nice internal API for setting values in an RFC822Message to + This provides a nice internal API for setting values in an Message to reduce boilerplate. If a value is None, do nothing. If a value contains a newline, indent it (may produce a warning in the future). """ - message: email.message.EmailMessage + message: email.message.Message def __setitem__(self, name: str, value: str | None) -> None: if not value: return - if '\n' in value: - msg = f'"{name}" should not be multiline; indenting to avoid breakage' - warnings.warn(msg, ConfigurationWarning, stacklevel=2) - value = value.replace('\n', '\n ') self.message[name] = value -class RFC822Message(email.message.EmailMessage): - """Python-flavored RFC 822 message implementation.""" +class MetadataPolicy(email.policy.Compat32): + def fold(self, name: str, value: str) -> str: + size = len(name) + 2 + value = value.replace('\n', '\n' + ' ' * size) + return f'{name}: {value}\n' - __slots__ = () - - def __init__(self) -> None: - super().__init__(email.policy.compat32) - - def __str__(self) -> str: - return bytes(self).decode('utf-8') + def fold_binary(self, name: str, value: str) -> bytes: + return self.fold(name, value).encode('utf-8') class DataFetcher: @@ -618,14 +611,10 @@ def __setattr__(self, name: str, value: Any) -> None: self._update_dynamic(value) super().__setattr__(name, value) - def as_rfc822(self) -> RFC822Message: - message = RFC822Message() - self.write_to_rfc822(message) - return message - - def write_to_rfc822(self, message: email.message.EmailMessage) -> None: # noqa: C901 + def as_rfc822(self) -> email.message.Message: # noqa: C901 self.validate(warn=False) + message = email.message.Message(policy=MetadataPolicy()) smart_message = _SmartMessageSetter(message) smart_message['Metadata-Version'] = self.metadata_version @@ -686,6 +675,8 @@ def write_to_rfc822(self, message: email.message.EmailMessage) -> None: # noqa: raise ConfigurationError(msg) smart_message['Dynamic'] = field + return message + def _name_list(self, people: list[tuple[str, str | None]]) -> str: return ', '.join(name for name, email_ in people if not email_) diff --git a/tests/test_rfc822.py b/tests/test_rfc822.py index cd2ddb5..1e8021a 100644 --- a/tests/test_rfc822.py +++ b/tests/test_rfc822.py @@ -2,7 +2,7 @@ from __future__ import annotations -import re +import email.message import textwrap import pytest @@ -32,6 +32,13 @@ Foo2: Bar2 """, ), + # Unicode + ( + [ + ('Foo', 'Unicøde'), + ], + 'Foo: Unicøde\n', + ), # None ( [ @@ -88,34 +95,31 @@ """\ ItemA: ValueA ItemB: ValueB1 - ValueB2 - ValueB3 + ValueB2 + ValueB3 ItemC: ValueC """, ), ], ) def test_headers(items: list[tuple[str, str]], data: str) -> None: - message = pyproject_metadata.RFC822Message() + message = email.message.Message(policy=pyproject_metadata.MetadataPolicy()) smart_message = pyproject_metadata._SmartMessageSetter(message) for name, value in items: - if value and '\n' in value: - msg = '"ItemB" should not be multiline; indenting to avoid breakage' - with pytest.warns( - pyproject_metadata.ConfigurationWarning, match=re.escape(msg) - ): - smart_message[name] = value - else: - smart_message[name] = value + smart_message[name] = value data = textwrap.dedent(data) + '\n' assert str(message) == data assert bytes(message) == data.encode() + assert email.message_from_string(str(message)).items() == [ + (a, '\n '.join(b.splitlines())) for a, b in items if b is not None + ] + def test_body() -> None: - message = pyproject_metadata.RFC822Message() + message = email.message.Message(policy=pyproject_metadata.MetadataPolicy()) message['ItemA'] = 'ValueA' message['ItemB'] = 'ValueB' @@ -134,7 +138,7 @@ def test_body() -> None: dolor id elementum. Ut bibendum nunc interdum neque interdum, vel tincidunt lacus blandit. Ut volutpat sollicitudin dapibus. Integer vitae lacinia ex, eget finibus nulla. Donec sit amet ante in neque pulvinar faucibus sed nec justo. - Fusce hendrerit massa libero, sit amet pulvinar magna tempor quis. + Fusce hendrerit massa libero, sit amet pulvinar magna tempor quis. ø """) ) @@ -155,9 +159,13 @@ def test_body() -> None: dolor id elementum. Ut bibendum nunc interdum neque interdum, vel tincidunt lacus blandit. Ut volutpat sollicitudin dapibus. Integer vitae lacinia ex, eget finibus nulla. Donec sit amet ante in neque pulvinar faucibus sed nec justo. - Fusce hendrerit massa libero, sit amet pulvinar magna tempor quis. + Fusce hendrerit massa libero, sit amet pulvinar magna tempor quis. ø """) + new_message = email.message_from_string(str(message)) + assert new_message.items() == message.items() + assert new_message.get_payload() == message.get_payload() + def test_convert_optional_dependencies() -> None: metadata = pyproject_metadata.StandardMetadata.from_pyproject(