diff --git a/internal/pypi/pypi.go b/internal/pypi/pypi.go index 47212f1..d493001 100644 --- a/internal/pypi/pypi.go +++ b/internal/pypi/pypi.go @@ -449,6 +449,10 @@ func parsePEP508(dep string) (string, string) { for i, c := range dep { if c == '>' || c == '<' || c == '=' || c == '~' || c == '!' || c == ';' { name := strings.TrimSpace(dep[:i]) + parenthesized := strings.HasSuffix(name, "(") + if parenthesized { + name = strings.TrimSpace(strings.TrimSuffix(name, "(")) + } // Remove extras if idx := strings.Index(name, "["); idx >= 0 { name = name[:idx] @@ -461,6 +465,9 @@ func parsePEP508(dep string) (string, string) { rest = rest[:idx] } version = strings.TrimSpace(rest) + if parenthesized { + version = strings.TrimSpace(strings.TrimSuffix(version, ")")) + } } return name, version } diff --git a/internal/pypi/pypi_test.go b/internal/pypi/pypi_test.go index b7aaf13..6def2c5 100644 --- a/internal/pypi/pypi_test.go +++ b/internal/pypi/pypi_test.go @@ -437,6 +437,10 @@ func TestParsePEP508(t *testing.T) { wantVersion string }{ {"requests>=2.0", "requests", ">=2.0"}, + {"requests (>=2.26,<3.0)", "requests", ">=2.26,<3.0"}, + {"packaging (>=24.2)", "packaging", ">=24.2"}, + {"platformdirs (>=3.0.0,<5)", "platformdirs", ">=3.0.0,<5"}, + {"importlib-metadata[perf] (>=6.0); python_version < '3.12'", "importlib-metadata", ">=6.0"}, {"requests[security]>=2.0", "requests", ">=2.0"}, {"Django>=3.0,<4.0", "Django", ">=3.0,<4.0"}, {"pytest", "pytest", ""}, @@ -457,6 +461,24 @@ func TestParsePEP508(t *testing.T) { } } +func BenchmarkParsePEP508(b *testing.B) { + inputs := []string{ + "requests>=2.0", + "requests[security] (>=2.26,<3.0); python_version < '3.12'", + "platformdirs (>=3.0.0,<5)", + } + + b.ReportAllocs() + for i := 0; i < b.N; i++ { + for _, input := range inputs { + name, _ := parsePEP508(input) + if name == "" { + b.Fatal("empty dependency name") + } + } + } +} + func TestPipCompileRequirementsIn(t *testing.T) { depMap := parseFixture(t, "../../testdata/pypi/pip-compile/requirements.in", "requirements.in", &requirementsTxtParser{}, 10) diff --git a/manifests_test.go b/manifests_test.go index 5787705..c1558c1 100644 --- a/manifests_test.go +++ b/manifests_test.go @@ -502,6 +502,56 @@ func TestPURL(t *testing.T) { t.Error("express dependency not found") } +func TestParsePEP508ParenthesizedRequirements(t *testing.T) { + content, err := os.ReadFile("testdata/pypi/pep508-parenthesized/pyproject.toml") + if err != nil { + t.Fatalf("failed to read fixture: %v", err) + } + + result, err := Parse("pyproject.toml", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + if len(result.Dependencies) != 4 { + t.Fatalf("expected 4 dependencies, got %d", len(result.Dependencies)) + } + + expected := map[string]struct { + version string + purl string + }{ + "requests": {version: ">=2.26,<3.0", purl: "pkg:pypi/requests"}, + "packaging": {version: ">=24.2", purl: "pkg:pypi/packaging"}, + "platformdirs": {version: ">=3.0.0,<5", purl: "pkg:pypi/platformdirs"}, + "importlib-metadata": { + version: ">=6.0", + purl: "pkg:pypi/importlib-metadata", + }, + } + + dependencies := make(map[string]Dependency, len(result.Dependencies)) + for _, dependency := range result.Dependencies { + if _, exists := dependencies[dependency.Name]; exists { + t.Errorf("duplicate dependency %q", dependency.Name) + } + dependencies[dependency.Name] = dependency + } + + for name, want := range expected { + dependency, ok := dependencies[name] + if !ok { + t.Errorf("expected dependency %q", name) + continue + } + if dependency.Version != want.version { + t.Errorf("%s version = %q, want %q", dependency.Name, dependency.Version, want.version) + } + if dependency.PURL != want.purl { + t.Errorf("%s PURL = %q, want %q", dependency.Name, dependency.PURL, want.purl) + } + } +} + func TestRegistryURLNotIncludedForDefaultRegistry(t *testing.T) { // Test that default registry URLs don't add repository_url qualifier testCases := []struct { diff --git a/testdata/pypi/pep508-parenthesized/pyproject.toml b/testdata/pypi/pep508-parenthesized/pyproject.toml new file mode 100644 index 0000000..01c37b0 --- /dev/null +++ b/testdata/pypi/pep508-parenthesized/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "pep508-parenthesized" +version = "1.0.0" +dependencies = [ + "requests (>=2.26,<3.0)", + "packaging (>=24.2)", + "platformdirs (>=3.0.0,<5)", + "importlib-metadata[perf] (>=6.0); python_version < '3.12'", +]