Skip to content

Add PEP 440 version comparison for pypi scheme#26

Open
andrew wants to merge 4 commits into
mainfrom
pypi-prerelease-24
Open

Add PEP 440 version comparison for pypi scheme#26
andrew wants to merge 4 commits into
mainfrom
pypi-prerelease-24

Conversation

@andrew

@andrew andrew commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #24.

parseDotSeparated silently dropped non-numeric segment suffixes, so 5.2b1 parsed as 5.0.0 and every PEP 440 prerelease form (a, b, rc, .dev, .post) collapsed to the release. There was also no pypi case in CompareWithScheme, and Range.Contains used generic comparison regardless of scheme, so even a correct comparator would not have been reached.

This adds a PEP 440 comparator (pypi.go) covering epoch, release-segment padding, dev < a < b < rc < release < post ordering, the spelling and separator variants the spec allows, and local version segments. Range now records the scheme it was parsed under and Contains dispatches to the matching comparator; intersectConsecutiveIntervals uses the same comparator when deciding whether a paired >=X | <Y interval is empty, so [5.1, 5.2b1) is no longer thrown away. parseDotSeparated splits a trailing letter suffix off a segment instead of discarding it, which fixes the generic Normalize path.

New tests in pypi_test.go cover the three reported cases plus the ordering chain from the PEP 440 spec, normalization equivalences, epochs, local versions, and range containment through both vers:pypi/ and native syntax.

The scheme-on-Range change also means maven and nuget containment now use their own comparators, which fixes a couple of latent cases noted in #25, though the rest of #25 (deb, rpm, gem) is out of scope here.

parseDotSeparated silently dropped non-numeric segment suffixes, so
5.2b1 parsed as 5.0.0 and every PEP 440 prerelease form collapsed to
the release. There was also no pypi case in CompareWithScheme, and
Range.Contains used generic comparison regardless of scheme.

Add a PEP 440 comparator covering epoch, release padding, pre/post/dev
ordering, spelling and separator normalization, and local version
segments. Store the scheme on Range so Contains can dispatch to it.
Split trailing letter suffixes in parseDotSeparated instead of
discarding them.

Fixes #24
@andrew andrew requested a review from Copilot July 10, 2026 17:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Range.IsEmpty used generic comparison, so a pypi range like
[1.0.dev1, 1.0a1) reported empty and serialized as vers:pypi/.
Use the scheme comparator there too.

Scheme was not set on ranges from the wildcard path, ParseNative
sub-parsers that bypass parseConstraints (including pypi ~=), or
the results of Union/Intersect/Exclude. Stamp it in each of those
places so Contains and IsEmpty on the result use the right rules.

Replace the 1<<30 positive-infinity sentinel in the PEP 440
comparator with explicit hasPre/hasPost/hasDev flags. Numeric
components are unbounded in PEP 440 and a large enough dev number
sorted the wrong side of an absent one.
andrew added 2 commits July 10, 2026 19:06
…merics as strings

Range.Intersect and Range.Union built results using generic
comparison, so intersecting >=1.0.dev1 with <1.0a1 under pypi
discarded the resulting interval as empty. Add cmp-taking variants
of Interval.Intersect/Union/Overlaps/Adjacent and mergeIntervals,
and have Range.Intersect/Union use the scheme comparator.

Store PEP 440 numeric components (epoch, release segments, pre/post/
dev numbers, numeric local segments) as digit strings and compare
them by length then lexically. Values that overflow int no longer
collapse to zero, and large local segments stay numeric.
…equality for Union exclusions

parsePypiRange delegated ~= to the gem pessimistic algorithm, which
counts raw dots (so .dev/.post added phantom segments) and drops the
epoch. ~=1.4.dev1 produced <1.5 instead of <2, and ~=1!1.4.5 lost the
epoch on the upper bound. Add a pypi-specific handler that reads the
release segments and epoch via parsePEP440 and increments the
second-last release segment. Stop trimming trailing zeros from the
parsed release so ~=2.0.dev1 sees two segments; cmpNumStrSlice already
treats missing segments as zero so comparison is unchanged.

Range.Union intersected exclusions by string equality, so unioning
ranges that exclude 1.5 and 1.5.0 respectively dropped the exclusion
even though those are the same version under PEP 440. Compare with
the scheme comparator instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PyPI prerelease versions are mis-parsed (5.2b1 becomes 5.0.0)

2 participants