Skip to content

Commit

Permalink
feat: support METADATA 2.1+ json format
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
  • Loading branch information
henryiii committed Sep 23, 2024
1 parent 203a419 commit 61a4930
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 13 deletions.
50 changes: 45 additions & 5 deletions pyproject_metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

__version__ = '0.9.0b4'


__all__ = [
'ConfigurationError',
'ConfigurationWarning',
Expand Down Expand Up @@ -110,6 +111,38 @@ def __setitem__(self, name: str, value: str | None) -> None:
return
self.message[name] = value

def set_payload(self, payload: str) -> None:
self.message.set_payload(payload)


@dataclasses.dataclass
class _JSonMessageSetter:
"""
This provides an API to build a JSON message output. Line breaks are
preserved this way.
"""

data: dict[str, str | list[str]]

def __setitem__(self, name: str, value: str | None) -> None:
name = name.lower()
key = name.replace('-', '_')

if value is None:
return

Check warning on line 132 in pyproject_metadata/__init__.py

View check run for this annotation

Codecov / codecov/patch

pyproject_metadata/__init__.py#L132

Added line #L132 was not covered by tests

if name == 'keywords':
self.data[key] = value.split(',')
elif name in constants.KNOWN_MULTIUSE:
entry = self.data.get(key, [])
assert isinstance(entry, list)
entry.append(value)
else:
self.data[key] = value

def set_payload(self, payload: str) -> None:
self['description'] = payload


class RFC822Policy(email.policy.EmailPolicy):
"""
Expand Down Expand Up @@ -365,13 +398,20 @@ def from_pyproject(

def as_rfc822(self) -> RFC822Message:
message = RFC822Message()
self.write_to_rfc822(message)
smart_message = _SmartMessageSetter(message)
self._write_metadata(smart_message)
return message

def write_to_rfc822(self, message: email.message.Message) -> None: # noqa: C901, PLR0912
self.validate(warn=False)
def as_json(self) -> dict[str, str | list[str]]:
message: dict[str, str | list[str]] = {}
smart_message = _JSonMessageSetter(message)
self._write_metadata(smart_message)
return message

smart_message = _SmartMessageSetter(message)
def _write_metadata( # noqa: PLR0912 C901
self, smart_message: _SmartMessageSetter | _JSonMessageSetter
) -> None:
self.validate(warn=False)

smart_message['Metadata-Version'] = self.auto_metadata_version
smart_message['Name'] = self.name
Expand Down Expand Up @@ -422,7 +462,7 @@ def write_to_rfc822(self, message: email.message.Message) -> None: # noqa: C901
if self.readme:
if self.readme.content_type:
smart_message['Description-Content-Type'] = self.readme.content_type
message.set_payload(self.readme.text)
smart_message.set_payload(self.readme.text)
# Core Metadata 2.2
if self.auto_metadata_version != '2.1':
for field in self.dynamic_metadata:
Expand Down
34 changes: 26 additions & 8 deletions pyproject_metadata/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@


__all__ = [
'KNOWN_BUILD_SYSTEM_FIELDS',
'KNOWN_METADATA_FIELDS',
'KNOWN_METADATA_VERSIONS',
'KNOWN_METADATA_VERSIONS',
'KNOWN_MULTIUSE',
'KNOWN_PROJECT_FIELDS',
'KNOWN_TOPLEVEL_FIELDS',
'PRE_SPDX_METADATA_VERSIONS',
'PROJECT_TO_METADATA',
'KNOWN_TOPLEVEL_FIELDS',
'KNOWN_BUILD_SYSTEM_FIELDS',
'KNOWN_PROJECT_FIELDS',
'KNOWN_METADATA_FIELDS',
]


Expand Down Expand Up @@ -51,8 +52,8 @@ def __dir__() -> list[str]:
'classifier',
'description',
'description-content-type',
'download-url', # Not specified via pyproject standards 'dynamic', # Can't be in dynamic
'dynamic',
'download-urL', # Not specified via pyproject standards
'dynamic', # Can't be in dynamic
'home-page', # Not specified via pyproject standards
'keywords',
'license',
Expand All @@ -63,11 +64,11 @@ def __dir__() -> list[str]:
'metadata-version',
'name', # Can't be in dynamic
'obsoletes', # Deprecated
'obsoletes-dist', # Rarly used
'obsoletes-dist', # Rarely used
'platform', # Not specified via pyproject standards
'project-url',
'provides', # Deprecated
'provides-dist', # Rarly used
'provides-dist', # Rarely used
'provides-extra',
'requires', # Deprecated
'requires-dist',
Expand All @@ -77,3 +78,20 @@ def __dir__() -> list[str]:
'supported-platform', # Not specified via pyproject standards
'version', # Can't be in dynamic
}

KNOWN_MULTIUSE = {
'dynamic',
'platform',
'provides-extra',
'supported-platform',
'license-file',
'classifier',
'requires-dist',
'requires-external',
'project-url',
'provides-dist',
'obsoletes-dist',
'requires', # Deprecated
'obsoletes', # Deprecated
'provides', # Deprecated
}
25 changes: 25 additions & 0 deletions tests/test_standard_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,31 @@ def test_readme_content_type_unknown(monkeypatch: pytest.MonkeyPatch) -> None:
pyproject_metadata.StandardMetadata.from_pyproject(tomllib.load(f))


def test_as_json(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(DIR / 'packages/full-metadata')

with open('pyproject.toml', 'rb') as f:
metadata = pyproject_metadata.StandardMetadata.from_pyproject(tomllib.load(f))
core_metadata = metadata.as_json()

assert core_metadata == {
'author': 'Example!',
'author_email': 'Unknown <example@example.com>',
'description': 'some readme 👋\n',
'description_content_type': 'text/markdown',
'home_page': 'example.com',
'keywords': ['trampolim', 'is', 'interesting'],
'license': 'some license text',
'maintainer': '',
'maintainer_email': 'Other Example <other@example.com>',
'metadata_version': '2.1',
'name': 'full_metadata',
'requires_python': '>=3.8',
'summary': 'A package with all the metadata :)',
'version': '3.2.1',
}


def test_as_rfc822(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(DIR / 'packages/full-metadata')

Expand Down

0 comments on commit 61a4930

Please sign in to comment.