Skip to content

Commit

Permalink
Unify {to/from}_pkg_info with {to/from}_dist_info_metadata
Browse files Browse the repository at this point in the history
As discussed in #383 instead of having 2 separated sets of methods
(one for `PKG-INFO` files in sdists and one for `METADATA` files in wheels)
we can have a single pair to/from functions with an
`allow_unfilled_dynamic` keyword argument.
  • Loading branch information
abravalheri committed Feb 2, 2022
1 parent 1ed4123 commit 1ca3573
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 28 deletions.
36 changes: 16 additions & 20 deletions packaging/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,28 +200,21 @@ def _parse_pkg_info(cls, pkg_info: bytes) -> Iterable[Tuple[str, Any]]:
yield ("description", str(info.get_payload(decode=True), "utf-8"))

@classmethod
def from_pkg_info(cls: Type[T], pkg_info: bytes) -> T:
def from_pkg_info(
cls: Type[T], pkg_info: bytes, *, allow_unfilled_dynamic: bool = True
) -> T:
"""Parse PKG-INFO data."""

attrs = cls._process_attrs(cls._parse_pkg_info(pkg_info))
obj = cls(**dict(attrs))
obj._validate_required_fields()
obj._validate_dynamic()
return obj

@classmethod
def from_dist_info_metadata(cls: Type[T], metadata_source: bytes) -> T:
"""Parse METADATA data."""
obj._validate(allow_unfilled_dynamic)

obj = cls.from_pkg_info(metadata_source)
obj._validate_final_metadata()
return obj

def to_pkg_info(self) -> bytes:
def to_pkg_info(self, *, allow_unfilled_dynamic: bool = True) -> bytes:
"""Generate PKG-INFO data."""

self._validate_required_fields()
self._validate_dynamic()
self._validate(allow_unfilled_dynamic)

info = EmailMessage(self._PARSING_POLICY)
info.add_header("Metadata-Version", self.metadata_version)
Expand Down Expand Up @@ -251,12 +244,6 @@ def to_pkg_info(self) -> bytes:

return info.as_bytes()

def to_dist_info_metadata(self) -> bytes:
"""Generate METADATA data."""

self._validate_final_metadata()
return self.to_pkg_info()

# --- Auxiliary Methods and Properties ---
# Not part of the API, but can be overwritten by subclasses
# (useful when providing a prof-of-concept for new PEPs)
Expand Down Expand Up @@ -343,16 +330,25 @@ def _unescape_description(cls, content: str) -> str:
continuation = (line.lstrip("|") for line in lines[1:])
return "\n".join(chain(lines[:1], continuation))

def _validate(self, allow_unfilled_dynamic: bool) -> bool:
self._validate_required_fields()
self._validate_dynamic()
if not allow_unfilled_dynamic:
self._validate_unfilled_dynamic()

return True

def _validate_dynamic(self) -> bool:
for item in self.dynamic:
field = _field_name(item)
if not hasattr(self, field):
raise InvalidCoreMetadataField(item)
if field in self._NOT_DYNAMIC:
raise InvalidDynamicField(item)

return True

def _validate_final_metadata(self) -> bool:
def _validate_unfilled_dynamic(self) -> bool:
unresolved = [k for k in self.dynamic if not getattr(self, _field_name(k))]
if unresolved:
raise UnfilledDynamicFields(unresolved)
Expand Down
17 changes: 9 additions & 8 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import tarfile
from email.policy import compat32
from functools import partial
from hashlib import md5
from itertools import chain
from pathlib import Path
Expand Down Expand Up @@ -207,11 +208,11 @@ def test_parsing(self, spec: str) -> None:
text = bytes(dedent(example["file_contents"]), "UTF-8")
pkg_info = CoreMetadata.from_pkg_info(text)
if example["is_final_metadata"]:
metadata = CoreMetadata.from_dist_info_metadata(text)
metadata = CoreMetadata.from_pkg_info(text, allow_unfilled_dynamic=False)
assert_equal_metadata(metadata, pkg_info)
if example["has_dynamic_fields"]:
with pytest.raises(UnfilledDynamicFields):
CoreMetadata.from_dist_info_metadata(text)
CoreMetadata.from_pkg_info(text, allow_unfilled_dynamic=False)
for field in ("requires_dist", "provides_dist", "obsoletes_dist"):
for value in getattr(pkg_info, field):
assert isinstance(value, Requirement)
Expand All @@ -225,10 +226,10 @@ def test_serliazing(self, spec: str) -> None:
text = bytes(dedent(example["file_contents"]), "UTF-8")
pkg_info = CoreMetadata.from_pkg_info(text)
if example["is_final_metadata"]:
assert isinstance(pkg_info.to_dist_info_metadata(), bytes)
assert isinstance(pkg_info.to_pkg_info(allow_unfilled_dynamic=False), bytes)
if example["has_dynamic_fields"]:
with pytest.raises(UnfilledDynamicFields):
pkg_info.to_dist_info_metadata()
pkg_info.to_pkg_info(allow_unfilled_dynamic=False)
pkg_info_text = pkg_info.to_pkg_info()
assert isinstance(pkg_info_text, bytes)
# Make sure generated document is not empty
Expand Down Expand Up @@ -274,14 +275,14 @@ class TestIntegration:
@pytest.mark.parametrize("pkg, version", examples())
def test_parse(self, pkg: str, version: str) -> None:
for dist in download_dists(pkg, version):
from_ = CoreMetadata.from_pkg_info
to_ = CoreMetadata.to_pkg_info
if dist.suffix == ".whl":
orig = read_metadata(dist)
from_ = CoreMetadata.from_dist_info_metadata
to_ = CoreMetadata.to_dist_info_metadata
from_ = partial(from_, allow_unfilled_dynamic=False)
to_ = partial(to_, allow_unfilled_dynamic=False)
else:
orig = read_pkg_info(dist)
from_ = CoreMetadata.from_pkg_info
to_ = CoreMetadata.to_pkg_info

# Given PKG-INFO or METADATA from existing packages on PyPI
# - Make sure they can be parsed
Expand Down

0 comments on commit 1ca3573

Please sign in to comment.