Skip to content

Commit 3b20e0f

Browse files
feat: support writing metadata (#846)
* feat: support writing metadata Signed-off-by: Henry Schreiner <[email protected]> * refactor: remove JSON output option Signed-off-by: Henry Schreiner <[email protected]> * tests: add a few more tests from pyproject-metadata Signed-off-by: Henry Schreiner <[email protected]> * Apply suggestions from code review Co-authored-by: Brett Cannon <[email protected]> * chore: apply pre-commit changes Signed-off-by: Henry Schreiner <[email protected]> --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Brett Cannon <[email protected]>
1 parent 888eb14 commit 3b20e0f

File tree

2 files changed

+392
-0
lines changed

2 files changed

+392
-0
lines changed

src/packaging/metadata.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,45 @@ def _get_payload(msg: email.message.Message, source: bytes | str) -> str:
283283
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
284284

285285

286+
# This class is for writing RFC822 messages
287+
class RFC822Policy(email.policy.EmailPolicy):
288+
"""
289+
This is :class:`email.policy.EmailPolicy`, but with a simple ``header_store_parse``
290+
implementation that handles multi-line values, and some nice defaults.
291+
"""
292+
293+
utf8 = True
294+
mangle_from_ = False
295+
max_line_length = 0
296+
297+
def header_store_parse(self, name: str, value: str) -> tuple[str, str]:
298+
size = len(name) + 2
299+
value = value.replace("\n", "\n" + " " * size)
300+
return (name, value)
301+
302+
303+
# This class is for writing RFC822 messages
304+
class RFC822Message(email.message.EmailMessage):
305+
"""
306+
This is :class:`email.message.EmailMessage` with two small changes: it defaults to
307+
our `RFC822Policy`, and it correctly writes unicode when being called
308+
with `bytes()`.
309+
"""
310+
311+
def __init__(self) -> None:
312+
super().__init__(policy=RFC822Policy())
313+
314+
def as_bytes(
315+
self, unixfrom: bool = False, policy: email.policy.Policy | None = None
316+
) -> bytes:
317+
"""
318+
Return the bytes representation of the message.
319+
320+
This handles unicode encoding.
321+
"""
322+
return self.as_string(unixfrom, policy=policy).encode("utf-8")
323+
324+
286325
def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
287326
"""Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``).
288327
@@ -860,3 +899,35 @@ def from_email(cls, data: bytes | str, *, validate: bool = True) -> Metadata:
860899
"""``Provides`` (deprecated)"""
861900
obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
862901
"""``Obsoletes`` (deprecated)"""
902+
903+
def as_rfc822(self) -> RFC822Message:
904+
"""
905+
Return an RFC822 message with the metadata.
906+
"""
907+
message = RFC822Message()
908+
self._write_metadata(message)
909+
return message
910+
911+
def _write_metadata(self, message: RFC822Message) -> None:
912+
"""
913+
Return an RFC822 message with the metadata.
914+
"""
915+
for name, validator in self.__class__.__dict__.items():
916+
if isinstance(validator, _Validator) and name != "description":
917+
value = getattr(self, name)
918+
email_name = _RAW_TO_EMAIL_MAPPING[name]
919+
if value is not None:
920+
if email_name == "project-url":
921+
for label, url in value.items():
922+
message[email_name] = f"{label}, {url}"
923+
elif email_name == "keywords":
924+
message[email_name] = ",".join(value)
925+
elif isinstance(value, list):
926+
for item in value:
927+
message[email_name] = str(item)
928+
else:
929+
message[email_name] = str(value)
930+
931+
# The description is a special case because it is in the body of the message.
932+
if self.description is not None:
933+
message.set_payload(self.description)

0 commit comments

Comments
 (0)