fix(python): keep epoch-pinned requirements in the SBOM (#5161)

parsePinnedVersion rejected any version constraint containing "!" to
skip the "!=" exclusion operator. PEP 440 epochs use "!" as well
(e.g. "1!2.0.0"), so an exact pin like "pkg == 1!2.0.0" was treated as
unparseable and returned "". parseRequirementsTxt drops a requirement
whose version resolves to "", so the package never made it into the
SBOM at all in either the pinned or guessed path.

Check for the "!=" operator as a substring instead of the bare "!"
character, which leaves the epoch separator alone.

Signed-off-by: Sueun Cho <sueun.dev@gmail.com>
This commit is contained in:
Sueun Cho 2026-08-20 22:06:17 +09:00 committed by GitHub
parent 78f39ee3a5
commit fd4796b0d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 24 additions and 1 deletions

View File

@ -188,7 +188,10 @@ func parseVersion(version string, guessFromConstraint bool) string {
func parsePinnedVersion(version string) string { func parsePinnedVersion(version string) string {
version = strings.TrimSpace(version) version = strings.TrimSpace(version)
if strings.ContainsAny(version, "*,<>!") { // a wildcard, range, or list of constraints is not a single pinned version. the "!=" exclusion
// operator is checked as a substring rather than by the bare "!" character, since a lone "!"
// is also the PEP 440 epoch separator (e.g. "1!2.0.0") and must not disqualify an exact pin.
if strings.ContainsAny(version, "*,<>") || strings.Contains(version, "!=") {
return "" return ""
} }

View File

@ -562,6 +562,26 @@ func Test_parseVersion(t *testing.T) {
version: "== 1.0a1.post2.dev3+local.1", version: "== 1.0a1.post2.dev3+local.1",
want: "1.0a1.post2.dev3+local.1", want: "1.0a1.post2.dev3+local.1",
}, },
{
name: "epoch",
version: "== 1!2.0.0",
want: "1!2.0.0",
},
{
name: "epoch with all segments combined",
version: "== 1!1.0a1.post2.dev3+local.1",
want: "1!1.0a1.post2.dev3+local.1",
},
{
name: "arbitrary equality with epoch",
version: "=== 1!2.0.0",
want: "1!2.0.0",
},
{
name: "bare exclusion is not a pin",
version: "!= 1.1.0",
want: "",
},
{ {
name: "resolve lowest, simple constraint", name: "resolve lowest, simple constraint",
version: " >= 1.0.0 ", version: " >= 1.0.0 ",