diff --git a/libs/patchwheel/parse.go b/libs/patchwheel/parse.go new file mode 100644 index 000000000..854f62fb8 --- /dev/null +++ b/libs/patchwheel/parse.go @@ -0,0 +1,45 @@ +package patchwheel + +import ( + "fmt" + "path/filepath" + "strings" +) + +// WheelInfo contains information extracted from a wheel filename +type WheelInfo struct { + Distribution string // Package distribution name + Version string // Package version + Tags []string // Python tags (python_tag, abi_tag, platform_tag) +} + +// ParseWheelFilename parses a wheel filename and extracts its components. +// Wheel filenames follow the pattern: {distribution}-{version}-{python_tag}-{abi_tag}-{platform_tag}.whl +func ParseWheelFilename(filename string) (*WheelInfo, error) { + base := filepath.Base(filename) + parts := strings.Split(base, "-") + if len(parts) < 5 || !strings.HasSuffix(parts[len(parts)-1], ".whl") { + return nil, fmt.Errorf("invalid wheel filename format: %s", filename) + } + + // The last three parts are always tags + tagStartIdx := len(parts) - 3 + + // Everything before the tags except the version is the distribution + versionIdx := tagStartIdx - 1 + + // Distribution may contain hyphens, so join all parts before the version + distribution := strings.Join(parts[:versionIdx], "-") + version := parts[versionIdx] + + // Extract tags (remove .whl from the last one) + tags := make([]string, 3) + copy(tags, parts[tagStartIdx:]) + tags[2] = strings.TrimSuffix(tags[2], ".whl") + + return &WheelInfo{ + Distribution: distribution, + Version: version, + Tags: tags, + }, nil +} diff --git a/libs/patchwheel/parse_test.go b/libs/patchwheel/parse_test.go new file mode 100644 index 000000000..3ef5073ba --- /dev/null +++ b/libs/patchwheel/parse_test.go @@ -0,0 +1,68 @@ +package patchwheel + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestParseWheelFilename tests the ParseWheelFilename function. +func TestParseWheelFilename(t *testing.T) { + tests := []struct { + filename string + wantDistribution string + wantVersion string + wantTags []string + wantErr bool + }{ + { + filename: "myproj-0.1.0-py3-none-any.whl", + wantDistribution: "myproj", + wantVersion: "0.1.0", + wantTags: []string{"py3", "none", "any"}, + wantErr: false, + }, + { + filename: "myproj-0.1.0+20240303123456-py3-none-any.whl", + wantDistribution: "myproj", + wantVersion: "0.1.0+20240303123456", + wantTags: []string{"py3", "none", "any"}, + wantErr: false, + }, + { + filename: "my-proj-with-hyphens-0.1.0-py3-none-any.whl", + wantDistribution: "my-proj-with-hyphens", + wantVersion: "0.1.0", + wantTags: []string{"py3", "none", "any"}, + wantErr: false, + }, + { + filename: "invalid-filename.txt", + wantDistribution: "", + wantVersion: "", + wantTags: nil, + wantErr: true, + }, + { + filename: "not-enough-parts-py3.whl", + wantDistribution: "", + wantVersion: "", + wantTags: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.filename, func(t *testing.T) { + info, err := ParseWheelFilename(tt.filename) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantDistribution, info.Distribution) + require.Equal(t, tt.wantVersion, info.Version) + require.Equal(t, tt.wantTags, info.Tags) + } + }) + } +} diff --git a/libs/patchwheel/patchwheel.go b/libs/patchwheel/patchwheel.go index 5f21d986d..42adbded0 100644 --- a/libs/patchwheel/patchwheel.go +++ b/libs/patchwheel/patchwheel.go @@ -125,44 +125,6 @@ func readFile(file *zip.File) ([]byte, error) { return io.ReadAll(rc) } -// WheelInfo contains information extracted from a wheel filename -type WheelInfo struct { - Distribution string // Package distribution name - Version string // Package version - Tags []string // Python tags (python_tag, abi_tag, platform_tag) -} - -// ParseWheelFilename parses a wheel filename and extracts its components. -// Wheel filenames follow the pattern: {distribution}-{version}-{python_tag}-{abi_tag}-{platform_tag}.whl -func ParseWheelFilename(filename string) (*WheelInfo, error) { - base := filepath.Base(filename) - parts := strings.Split(base, "-") - if len(parts) < 5 || !strings.HasSuffix(parts[len(parts)-1], ".whl") { - return nil, fmt.Errorf("invalid wheel filename format: %s", filename) - } - - // The last three parts are always tags - tagStartIdx := len(parts) - 3 - - // Everything before the tags except the version is the distribution - versionIdx := tagStartIdx - 1 - - // Distribution may contain hyphens, so join all parts before the version - distribution := strings.Join(parts[:versionIdx], "-") - version := parts[versionIdx] - - // Extract tags (remove .whl from the last one) - tags := make([]string, 3) - copy(tags, parts[tagStartIdx:]) - tags[2] = strings.TrimSuffix(tags[2], ".whl") - - return &WheelInfo{ - Distribution: distribution, - Version: version, - Tags: tags, - }, nil -} - // PatchWheel patches a Python wheel file by updating its version in METADATA and RECORD. // It returns the path to the new wheel. // The function is idempotent: repeated calls with the same input will produce the same output. diff --git a/libs/patchwheel/patchwheel_test.go b/libs/patchwheel/patchwheel_test.go index c8bc8f137..c8762bd63 100644 --- a/libs/patchwheel/patchwheel_test.go +++ b/libs/patchwheel/patchwheel_test.go @@ -105,67 +105,6 @@ func getWheel(t *testing.T, dir string) string { return matches[0] } -// TestParseWheelFilename tests the ParseWheelFilename function. -func TestParseWheelFilename(t *testing.T) { - tests := []struct { - filename string - wantDistribution string - wantVersion string - wantTags []string - wantErr bool - }{ - { - filename: "myproj-0.1.0-py3-none-any.whl", - wantDistribution: "myproj", - wantVersion: "0.1.0", - wantTags: []string{"py3", "none", "any"}, - wantErr: false, - }, - { - filename: "myproj-0.1.0+20240303123456-py3-none-any.whl", - wantDistribution: "myproj", - wantVersion: "0.1.0+20240303123456", - wantTags: []string{"py3", "none", "any"}, - wantErr: false, - }, - { - filename: "my-proj-with-hyphens-0.1.0-py3-none-any.whl", - wantDistribution: "my-proj-with-hyphens", - wantVersion: "0.1.0", - wantTags: []string{"py3", "none", "any"}, - wantErr: false, - }, - { - filename: "invalid-filename.txt", - wantDistribution: "", - wantVersion: "", - wantTags: nil, - wantErr: true, - }, - { - filename: "not-enough-parts-py3.whl", - wantDistribution: "", - wantVersion: "", - wantTags: nil, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.filename, func(t *testing.T) { - info, err := ParseWheelFilename(tt.filename) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, tt.wantDistribution, info.Distribution) - require.Equal(t, tt.wantVersion, info.Version) - require.Equal(t, tt.wantTags, info.Tags) - } - }) - } -} - func TestPatchWheel(t *testing.T) { pythonVersions := []string{"python3.9", "python3.10", "python3.11", "python3.12"} for _, py := range pythonVersions {