diff --git a/docs/metadata.rst b/docs/metadata.rst index c1c2767cd..6f18026d0 100644 --- a/docs/metadata.rst +++ b/docs/metadata.rst @@ -17,10 +17,10 @@ Usage .. doctest:: >>> from packaging.metadata import parse_email - >>> metadata = "Metadata-Version: 2.3\nName: packaging\nVersion: 24.0" + >>> metadata = "Metadata-Version: 2.6\nName: packaging\nVersion: 24.0" >>> raw, unparsed = parse_email(metadata) >>> raw["metadata_version"] - '2.3' + '2.6' >>> raw["name"] 'packaging' >>> raw["version"] diff --git a/src/packaging/metadata.py b/src/packaging/metadata.py index af91241c1..59ad15d3e 100644 --- a/src/packaging/metadata.py +++ b/src/packaging/metadata.py @@ -132,6 +132,8 @@ class RawMetadata(TypedDict, total=False): import_names: list[str] import_namespaces: list[str] + # Metadata 2.6 - PEP 808 (no new fields, behavior change for Dynamic) + # 'keywords' is special as it's a string in the core metadata spec, but we # represent it as a list. @@ -512,8 +514,20 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: # Keep the two values in sync. -_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"] -_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"] +_VALID_METADATA_VERSIONS = [ + "1.0", + "1.1", + "1.2", + "2.1", + "2.2", + "2.3", + "2.4", + "2.5", + "2.6", +] +_MetadataVersion = Literal[ + "1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6" +] _REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"]) diff --git a/tests/metadata/everything.metadata b/tests/metadata/everything.metadata index 83e8aa669..e1645ad2a 100644 --- a/tests/metadata/everything.metadata +++ b/tests/metadata/everything.metadata @@ -1,4 +1,4 @@ -Metadata-Version: 2.5 +Metadata-Version: 2.6 Name: BeagleVote Version: 1.0a2 Platform: ObscureUnix diff --git a/tests/test_metadata.py b/tests/test_metadata.py index f6783f93e..f2b759323 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -197,7 +197,7 @@ def test_complete(self) -> None: assert len(unparsed) == 1 # "ThisIsNotReal" key assert unparsed["thisisnotreal"] == ["Hello!"] assert len(raw) == 28 - assert raw["metadata_version"] == "2.5" + assert raw["metadata_version"] == "2.6" assert raw["name"] == "BeagleVote" assert raw["version"] == "1.0a2" assert raw["platforms"] == ["ObscureUnix", "RareDOS"] @@ -274,7 +274,7 @@ def test_repr(self) -> None: _RAW_EXAMPLE: RawMetadata = { - "metadata_version": "2.5", + "metadata_version": "2.6", "name": "packaging", "version": "2023.0.0", } @@ -301,7 +301,7 @@ def _invalid_with_cause( assert isinstance(exc.__cause__, cause) def test_from_email(self) -> None: - metadata_version = "2.5" + metadata_version = "2.6" meta = metadata.Metadata.from_email( f"Metadata-Version: {metadata_version}", validate=False ) @@ -311,7 +311,7 @@ def test_from_email(self) -> None: def test_from_email_empty_import_name(self) -> None: meta = metadata.Metadata.from_email( - "Metadata-Version: 2.5\nImport-Name:\n", validate=False + "Metadata-Version: 2.6\nImport-Name:\n", validate=False ) assert meta.import_names == [] @@ -447,7 +447,9 @@ def test_multi_value_unvalidated_attribute(self, attribute: str) -> None: assert getattr(meta, attribute) == values - @pytest.mark.parametrize("version", ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"]) + @pytest.mark.parametrize( + "version", ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] + ) def test_valid_metadata_version(self, version: str) -> None: meta = metadata.Metadata.from_raw({"metadata_version": version}, validate=False) @@ -644,6 +646,65 @@ def test_invalid_dynamic_value(self) -> None: with pytest.raises(metadata.InvalidMetadata): meta.dynamic # noqa: B018 + def test_pep808_dynamic_with_static_values(self) -> None: + """PEP 808 allows list/table fields to have static values + while their key is also listed in ``Dynamic``.""" + raw: RawMetadata = { + "metadata_version": "2.6", + "name": "packaging", + "version": "1.0.0", + "classifiers": ["Development Status :: 4 - Beta"], + "requires_dist": ["pytest"], + "provides_extra": ["test"], + "license_files": ["LICENSE"], + "project_urls": {"homepage": "example.com"}, + "import_names": ["packaging"], + "import_namespaces": ["pkg.ns"], + "dynamic": [ + "classifier", + "requires-dist", + "provides-extra", + "license-file", + "project-url", + "import-name", + "import-namespace", + ], + } + + meta = metadata.Metadata.from_raw(raw, validate=True) + assert meta.classifiers == ["Development Status :: 4 - Beta"] + assert meta.requires_dist == [requirements.Requirement("pytest")] + assert meta.provides_extra == [utils.canonicalize_name("test")] + assert meta.license_files == ["LICENSE"] + assert meta.project_urls == {"homepage": "example.com"} + assert meta.import_names == ["packaging"] + assert meta.import_namespaces == ["pkg.ns"] + assert meta.dynamic == [ + "classifier", + "requires-dist", + "provides-extra", + "license-file", + "project-url", + "import-name", + "import-namespace", + ] + + rfc822 = meta.as_rfc822() + assert ("classifier", "Development Status :: 4 - Beta") in rfc822.items() + assert ("requires-dist", "pytest") in rfc822.items() + assert ("provides-extra", "test") in rfc822.items() + assert ("license-file", "LICENSE") in rfc822.items() + assert ("project-url", "homepage, example.com") in rfc822.items() + assert ("import-name", "packaging") in rfc822.items() + assert ("import-namespace", "pkg.ns") in rfc822.items() + assert ("dynamic", "classifier") in rfc822.items() + assert ("dynamic", "requires-dist") in rfc822.items() + assert ("dynamic", "provides-extra") in rfc822.items() + assert ("dynamic", "license-file") in rfc822.items() + assert ("dynamic", "project-url") in rfc822.items() + assert ("dynamic", "import-name") in rfc822.items() + assert ("dynamic", "import-namespace") in rfc822.items() + @pytest.mark.parametrize("field_name", ["name", "version", "metadata-version"]) def test_disallowed_dynamic(self, field_name: str) -> None: meta = metadata.Metadata.from_raw({"dynamic": [field_name]}, validate=False) @@ -855,7 +916,7 @@ def test_write_metadata(self) -> None: meta = metadata.Metadata.from_raw(_RAW_EXAMPLE) written = meta.as_rfc822().as_string() assert ( - written == "metadata-version: 2.5\nname: packaging\nversion: 2023.0.0\n\n" + written == "metadata-version: 2.6\nname: packaging\nversion: 2023.0.0\n\n" ) def test_write_metadata_with_description(self) -> None: @@ -1005,7 +1066,7 @@ def test_modern_license(self) -> None: def test__import_names(self) -> None: meta = metadata.Metadata.from_raw( { - "metadata_version": "2.5", + "metadata_version": "2.6", "name": "full_metadata", "version": "3.2.1", "import_names": ["one", "two"], @@ -1015,7 +1076,7 @@ def test__import_names(self) -> None: core_metadata = meta.as_rfc822() assert core_metadata.items() == [ - ("metadata-version", "2.5"), + ("metadata-version", "2.6"), ("name", "full_metadata"), ("version", "3.2.1"), ("import-name", "one"), @@ -1028,7 +1089,7 @@ def test__import_names(self) -> None: def test_empty_import_names(self) -> None: meta = metadata.Metadata.from_raw( { - "metadata_version": "2.5", + "metadata_version": "2.6", "name": "full_metadata", "version": "3.2.1", "import_names": [], @@ -1037,7 +1098,7 @@ def test_empty_import_names(self) -> None: core_metadata = meta.as_rfc822() assert core_metadata.items() == [ - ("metadata-version", "2.5"), + ("metadata-version", "2.6"), ("name", "full_metadata"), ("version", "3.2.1"), ("import-name", ""),