Skip to content

Commit

Permalink
fix: add unicode and join multilines (#150)
Browse files Browse the repository at this point in the history
* fix: add unicode and join multilines

Signed-off-by: Henry Schreiner <[email protected]>

* fix: custom email policy

Signed-off-by: Henry Schreiner <[email protected]>

* chore: reduce diff

Signed-off-by: Henry Schreiner <[email protected]>

* fix: typing

Signed-off-by: Henry Schreiner <[email protected]>

* refactor: EmailMessage -> Message

Signed-off-by: Henry Schreiner <[email protected]>

---------

Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii authored Sep 13, 2024
1 parent 920c7da commit 319a5bd
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 37 deletions.
35 changes: 13 additions & 22 deletions pyproject_metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
__all__ = [
'ConfigurationError',
'ConfigurationWarning',
'RFC822Message',
'License',
'Readme',
'StandardMetadata',
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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_)

Expand Down
38 changes: 23 additions & 15 deletions tests/test_rfc822.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

import re
import email.message
import textwrap

import pytest
Expand Down Expand Up @@ -32,6 +32,13 @@
Foo2: Bar2
""",
),
# Unicode
(
[
('Foo', 'Unicøde'),
],
'Foo: Unicøde\n',
),
# None
(
[
Expand Down Expand Up @@ -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'
Expand All @@ -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. ø
""")
)

Expand All @@ -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(
Expand Down

0 comments on commit 319a5bd

Please sign in to comment.