diff --git a/packaging/metadata.py b/packaging/metadata.py index f8c38867..ca28ceb8 100644 --- a/packaging/metadata.py +++ b/packaging/metadata.py @@ -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) @@ -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) @@ -343,6 +330,14 @@ 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) @@ -350,9 +345,10 @@ def _validate_dynamic(self) -> bool: 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) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 98bf458f..ca0212e1 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -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 @@ -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) @@ -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 @@ -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