Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 15 additions & 3 deletions distribution/internal/distributions/distribution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
55 changes: 54 additions & 1 deletion distribution/parse_fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -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)
}
Loading