diff --git a/README.md b/README.md index 15af058..5d7badc 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ metadata = StandardMetadata.from_pyproject(parsed_pyproject, allow_extra_keys = print(metadata.entrypoints) # same fields as defined in PEP 621 pkg_info = metadata.as_rfc822() -print(str(pkg_info)) # core metadata +print(bytes(pkg_info).decode("utf-8")) # core metadata ``` diff --git a/pyproject_metadata/__init__.py b/pyproject_metadata/__init__.py index 9a38b23..56b13cf 100644 --- a/pyproject_metadata/__init__.py +++ b/pyproject_metadata/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -import collections import copy import dataclasses import email.utils @@ -117,35 +116,44 @@ class ConfigurationWarning(UserWarning): class RFC822Message: """Python-flavored RFC 822 message implementation.""" + __slots__ = ('_headers', '_body') + def __init__(self) -> None: - self.headers: collections.OrderedDict[str, list[str]] = ( - collections.OrderedDict() - ) - self.body: str | None = None + self._headers: list[tuple[str, str]] = [] + self._body: str | None = None + + def items(self) -> list[tuple[str, str]]: + return self._headers.copy() + + def get_all(self, name: str) -> None | list[str]: + return [v for k, v in self.items() if k == name] def __setitem__(self, name: str, value: str | None) -> None: if not value: return - if name not in self.headers: - self.headers[name] = [] - self.headers[name].append(value) + self._headers.append((name, value)) def __str__(self) -> str: text = '' - for name, entries in self.headers.items(): - for entry in entries: - lines = entry.strip('\n').split('\n') - text += f'{name}: {lines[0]}\n' - for line in lines[1:]: - text += ' ' * 8 + line + '\n' + for name, entry in self.items(): + lines = entry.strip('\n').split('\n') + text += f'{name}: {lines[0]}\n' + for line in lines[1:]: + text += ' ' * 8 + line + '\n' text += '\n' - if self.body: - text += self.body + if self._body: + text += self._body return text def __bytes__(self) -> bytes: return str(self).encode() + def get_payload(self) -> str | None: + return self._body + + def set_payload(self, body: str) -> None: + self._body = body + class DataFetcher: def __init__(self, data: Mapping[str, Any]) -> None: @@ -603,7 +611,7 @@ def write_to_rfc822(self, message: RFC822Message) -> None: # noqa: C901 if self.readme: if self.readme.content_type: message['Description-Content-Type'] = self.readme.content_type - message.body = self.readme.text + message.set_payload(self.readme.text) # Core Metadata 2.2 if self.metadata_version != '2.1': for field in self.dynamic: diff --git a/tests/test_rfc822.py b/tests/test_rfc822.py index 40b7236..1923c22 100644 --- a/tests/test_rfc822.py +++ b/tests/test_rfc822.py @@ -73,9 +73,9 @@ ], """\ ItemA: ValueA1 - ItemA: ValueA2 ItemB: ValueB ItemC: ValueC + ItemA: ValueA2 """, ), ( @@ -112,7 +112,8 @@ def test_body() -> None: message['ItemB'] = 'ValueB' message['ItemC'] = 'ValueC' - message.body = textwrap.dedent(""" + message.set_payload( + textwrap.dedent(""" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris congue semper fermentum. Nunc vitae tempor ante. Aenean aliquet posuere lacus non faucibus. In porttitor congue luctus. Vivamus eu dignissim orci. Donec egestas mi ac @@ -126,6 +127,7 @@ def test_body() -> None: finibus nulla. Donec sit amet ante in neque pulvinar faucibus sed nec justo. Fusce hendrerit massa libero, sit amet pulvinar magna tempor quis. """) + ) assert str(message) == textwrap.dedent("""\ ItemA: ValueA @@ -164,7 +166,7 @@ def test_convert_optional_dependencies() -> None: } ) message = metadata.as_rfc822() - requires = message.headers['Requires-Dist'] + requires = message.get_all('Requires-Dist') assert requires == [ 'foo; (os_name == "nt" or sys_platform == "win32") and extra == "test"', 'bar; os_name == "posix" and sys_platform == "linux" and extra == "test"', @@ -191,6 +193,6 @@ def test_convert_author_email() -> None: } ) message = metadata.as_rfc822() - assert message.headers['Author-Email'] == [ + assert message.get_all('Author-Email') == [ '"John Doe, Inc." , "Kate Doe, LLC." ' ] diff --git a/tests/test_standard_metadata.py b/tests/test_standard_metadata.py index 812d7d7..baed82b 100644 --- a/tests/test_standard_metadata.py +++ b/tests/test_standard_metadata.py @@ -713,42 +713,39 @@ def test_as_rfc822(monkeypatch: pytest.MonkeyPatch) -> None: with open('pyproject.toml', 'rb') as f: metadata = pyproject_metadata.StandardMetadata.from_pyproject(tomllib.load(f)) core_metadata = metadata.as_rfc822() - assert core_metadata.headers == { - 'Metadata-Version': ['2.1'], - 'Name': ['full_metadata'], - 'Summary': ['A package with all the metadata :)'], - 'Version': ['3.2.1'], - 'Keywords': ['trampolim,is,interesting'], - 'Home-page': ['example.com'], - 'Author': ['Example!'], - 'Author-Email': ['Unknown '], - 'Maintainer-Email': ['Other Example '], - 'License': ['some license text'], - 'Classifier': [ - 'Development Status :: 4 - Beta', - 'Programming Language :: Python', - ], - 'Project-URL': [ - 'Homepage, example.com', - 'Documentation, readthedocs.org', - 'Repository, github.com/some/repo', - 'Changelog, github.com/some/repo/blob/master/CHANGELOG.rst', - ], - 'Requires-Python': ['>=3.8'], - 'Provides-Extra': ['test'], - 'Requires-Dist': [ - 'dependency1', - 'dependency2>1.0.0', - 'dependency3[extra]', - 'dependency4; os_name != "nt"', - 'dependency5[other-extra]>1.0; os_name == "nt"', - 'test_dependency; extra == "test"', - 'test_dependency[test_extra]; extra == "test"', + assert core_metadata.items() == [ + ('Metadata-Version', '2.1'), + ('Name', 'full_metadata'), + ('Version', '3.2.1'), + ('Summary', 'A package with all the metadata :)'), + ('Keywords', 'trampolim,is,interesting'), + ('Home-page', 'example.com'), + ('Author', 'Example!'), + ('Author-Email', 'Unknown '), + ('Maintainer-Email', 'Other Example '), + ('License', 'some license text'), + ('Classifier', 'Development Status :: 4 - Beta'), + ('Classifier', 'Programming Language :: Python'), + ('Project-URL', 'Homepage, example.com'), + ('Project-URL', 'Documentation, readthedocs.org'), + ('Project-URL', 'Repository, github.com/some/repo'), + ('Project-URL', 'Changelog, github.com/some/repo/blob/master/CHANGELOG.rst'), + ('Requires-Python', '>=3.8'), + ('Requires-Dist', 'dependency1'), + ('Requires-Dist', 'dependency2>1.0.0'), + ('Requires-Dist', 'dependency3[extra]'), + ('Requires-Dist', 'dependency4; os_name != "nt"'), + ('Requires-Dist', 'dependency5[other-extra]>1.0; os_name == "nt"'), + ('Provides-Extra', 'test'), + ('Requires-Dist', 'test_dependency; extra == "test"'), + ('Requires-Dist', 'test_dependency[test_extra]; extra == "test"'), + ( + 'Requires-Dist', 'test_dependency[test_extra2]>3.0; os_name == "nt" and extra == "test"', - ], - 'Description-Content-Type': ['text/markdown'], - } - assert core_metadata.body == 'some readme 👋\n' + ), + ('Description-Content-Type', 'text/markdown'), + ] + assert core_metadata.get_payload() == 'some readme 👋\n' def test_as_rfc822_dynamic(monkeypatch: pytest.MonkeyPatch) -> None: @@ -757,12 +754,12 @@ def test_as_rfc822_dynamic(monkeypatch: pytest.MonkeyPatch) -> None: with open('pyproject.toml', 'rb') as f: metadata = pyproject_metadata.StandardMetadata.from_pyproject(tomllib.load(f)) core_metadata = metadata.as_rfc822() - assert dict(core_metadata.headers) == { - 'Metadata-Version': ['2.2'], - 'Name': ['dynamic-description'], - 'Version': ['1.0.0'], - 'Dynamic': ['description'], - } + assert core_metadata.items() == [ + ('Metadata-Version', '2.2'), + ('Name', 'dynamic-description'), + ('Version', '1.0.0'), + ('Dynamic', 'description'), + ] @pytest.mark.parametrize('metadata_version', ['2.1', '2.2', '2.3']) @@ -784,7 +781,7 @@ def test_as_rfc822_set_metadata(metadata_version: str) -> None: ) assert metadata.metadata_version == metadata_version - rfc822 = str(metadata.as_rfc822()) + rfc822 = bytes(metadata.as_rfc822()).decode('utf-8') assert f'Metadata-Version: {metadata_version}' in rfc822