From 8dfe7c5df58e66fb028393ad66d34d971ac5f453 Mon Sep 17 00:00:00 2001 From: nodivbyzero Date: Fri, 9 Oct 2026 11:49:49 -0700 Subject: [PATCH] Add Metadata 2.5/2.6 support, fix License-File multiplicity --- CHANGELOG.md | 11 ++++ .../internal/distributions/distribution.go | 18 +++++- distribution/parse_fixture_test.go | 55 ++++++++++++++++++- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de6713c..fabf4c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ mistaken for a safe patch upgrade. ## [Unreleased] +### Added + +- `distribution`: Metadata 2.5 (PEP 794) and 2.6 (PEP 808) are now recognized. + Metadata 2.5 adds `Import-Name` and `Import-Namespace` fields; 2.6 has no new + fields (behavior change for `Dynamic` only). + +### Fixed + +- `distribution`: `License-File` is now parsed as a multi-value field, matching + the PEP 639 spec. Previously it was incorrectly treated as single-value. + ## [0.11.0] - 2026-09-25 ### Breaking diff --git a/distribution/internal/distributions/distribution.go b/distribution/internal/distributions/distribution.go index 1c24d3f..2046577 100644 --- a/distribution/internal/distributions/distribution.go +++ b/distribution/internal/distributions/distribution.go @@ -141,9 +141,16 @@ var HeaderAttrs2_3 = HeaderAttrs2_2 // PEP 685 var HeaderAttrs2_4 = append(HeaderAttrs2_3, []HeaderAttr{ // PEP 639 {"License-Expression", "license_expression", false}, - {"License-File", "license_file", false}, + {"License-File", "license_files", true}, }...) +var HeaderAttrs2_5 = append(HeaderAttrs2_4, []HeaderAttr{ // PEP 794 + {"Import-Name", "import_names", true}, + {"Import-Namespace", "import_namespaces", true}, +}...) + +var HeaderAttrs2_6 = HeaderAttrs2_5 // PEP 808 (no new fields, behavior change for Dynamic) + var HeaderAttrs = map[string][]HeaderAttr{ "1.0": HeaderAttrs1_0, "1.1": HeaderAttrs1_1, @@ -153,6 +160,8 @@ var HeaderAttrs = map[string][]HeaderAttr{ "2.2": HeaderAttrs2_2, "2.3": HeaderAttrs2_3, "2.4": HeaderAttrs2_4, + "2.5": HeaderAttrs2_5, + "2.6": HeaderAttrs2_6, } type Distribution interface { @@ -205,8 +214,11 @@ type BaseDistribution struct { // version 2.2 Dynamic []string `json:"dynamic"` // version 2.4 - LicenseExpression string `json:"license_expression"` - LicenseFile string `json:"license_file"` + LicenseExpression string `json:"license_expression"` + LicenseFiles []string `json:"license_files"` + // version 2.5 + ImportNames []string `json:"import_names"` + ImportNamespaces []string `json:"import_namespaces"` } func (bd *BaseDistribution) GetHeaderAttrs() ([]HeaderAttr, error) { diff --git a/distribution/parse_fixture_test.go b/distribution/parse_fixture_test.go index 70bb69d..7cbf07c 100644 --- a/distribution/parse_fixture_test.go +++ b/distribution/parse_fixture_test.go @@ -168,7 +168,7 @@ func TestParse_WheelMultiValueFields(t *testing.T) { "provides_extra": {"extra-one", "extra-two"}, "project_urls": {"Homepage, https://example.invalid/gppfixture-alpha", "Source, https://example.invalid/gppfixture-alpha/src"}, "license_expression": {"MIT"}, - "license_file": {"LICENSE.txt"}, + "license_files": {"LICENSE.txt"}, "dynamic": {"requires-dist", "license-file"}, "description": {body}, "filetype": {"bdist_wheel"}, @@ -397,3 +397,56 @@ func TestParse_DirectoryExpansionWheelFirst(t *testing.T) { require.NotEqual(t, -1, sdistIdx, "sdist not found in results") assert.Less(t, wheelIdx, sdistIdx, "every wheel must come before every sdist") } + +// TestParse_WheelMetadata25 verifies parsing of Metadata-Version 2.5 (PEP 794) +// with Import-Name and Import-Namespace fields. +func TestParse_WheelMetadata25(t *testing.T) { + dir := t.TempDir() + wheelPath := filepath.Join(dir, "gppfixture_eta-1.0.0-py3-none-any.whl") + + header := strings.Join([]string{ + "Metadata-Version: 2.5", + "Name: gppfixture-eta", + "Version: 1.0.0", + "Import-Name: gppfixture_eta", + "Import-Name: gppfixture_eta.submodule", + "Import-Namespace: gppfixture_shared", + }, "\n") + body := "Fixture eta description.\n" + metadata := header + "\n\n" + body + + writeWheelFixture(t, wheelPath, []fixtureEntry{ + {name: "gppfixture_eta-1.0.0.dist-info/METADATA", contents: metadata}, + {name: "gppfixture_eta-1.0.0.dist-info/WHEEL", contents: "Wheel-Version: 1.0\nGenerator: test\nRoot-Is-Purelib: true\nTag: py3-none-any\n"}, + {name: "gppfixture_eta-1.0.0.dist-info/RECORD", contents: ""}, + }) + + sha2, blake2 := fileDigests(t, wheelPath) + + packages, err := distribution.Parse(wheelPath) + require.NoError(t, err) + require.Len(t, packages, 1) + pkg := packages[0] + + assert.Equal(t, "gppfixture_eta-1.0.0-py3-none-any.whl", pkg.BaseFilename) + assert.Equal(t, sha2, pkg.SHA2Digest) + assert.Equal(t, blake2, pkg.Blake2_256Digest) + + got := pkg.MetadataMap() + + want := map[string][]string{ + ":action": {"file_upload"}, + "protocol_version": {"1"}, + "name": {"gppfixture-eta"}, + "metadata_version": {"2.5"}, + "version": {"1.0.0"}, + "import_names": {"gppfixture_eta", "gppfixture_eta.submodule"}, + "import_namespaces": {"gppfixture_shared"}, + "description": {body}, + "filetype": {"bdist_wheel"}, + "pyversion": {"py3"}, + "sha256_digest": {sha2}, + "blake2_256_digest": {blake2}, + } + assert.Equal(t, want, got) +}