Add a public PEP 440 compliant VersionRange API#1182
Draft
notatallshaw wants to merge 4 commits into
Draft
Conversation
f645373 to
fee7bb1
Compare
fee7bb1 to
536a8a7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On top of #1120, promote the internal range helpers to a public
VersionRangeclass inpackaging.ranges. And push all the PEP 440 handling intoVersionRange, soSpecifierandSpecifierSetbecome light wrappers around it.A
VersionRangerepresents the set ofVersionvalues matched by aSpecifierorSpecifierSetas a union of disjoint intervals on the PEP 440 version ordering, and is closed under intersection, union, and complement.Set operations on a
VersionRangeanswer overlap, subset, and complement questions directly, without parsing strings or iterating versions.A resolver implementation can now defer all its PEP 440 logic to
packaging. I've focused on PubGrub as a real use case, but the same applies to other algorithms.Why a new class instead of extending SpecifierSet
SpecifierSetis not closed under union or complement:{V}(==Valso matchesV+local).>V.So
intersection/union/complementonSpecifierSetwould produce values that no SpecifierSet string can represent. Adding those methods toSpecifierSetwould require a partial API (returningNonefor the unsupported cases) and would break the existing contract that aSpecifierSetround-trips through its string form.A separate
VersionRangekeepsSpecifierSet's contract intact, and is itself closed under intersection, union, and complement. Conversion back toSpecifierSetviato_specifier_set()/to_specifier_sets()returnsNonewhen the range cannot be expressed.New methods on existing types
Specifier.to_range() -> VersionRangeSpecifierSet.to_range() -> VersionRangeConvenience wrappers around the
VersionRange.from_*classmethod factories. Results are cached on the specifier instance.I was a bit unsure on the name or if these are needed, we could pick one of:
spec.range()spec.to_range()spec.version_range()spec.to_version_range()VersionRange.from_specifier_set(spec)VersionRangeconstructors:VersionRangehas no direct constructor: it isn't a PEP 440 object, just a more general predicate onVersion. Construct one with a factory, or build aSpecifierSetfirst and call.to_range()on it.VersionRange.from_specifier(specifier): range for aSpecifier.VersionRange.from_specifier_set(specifier_set): range for aSpecifierSet(the intersection of every specifier in the set).VersionRange.empty(): the empty rangeVersionRange.full(): the unbounded rangeVersionRange.singleton(version): the strict singleton{version}(does not matchversion+local)Singleton is a borrowed term from PubGrub/uv, I'm not tied to it, but whatever is chosen it should not be confused with an actual
Versionas aVersionis not a range.Calling
VersionRange()directly raisesTypeError.Set algebra
intersection(other)/&union(other)/|complement()/~All return a new range; operands are not mutated.
Membership and filtering
version in range: membership; mirrorsSpecifierSet.__contains__.filter(iterable, key=None, prereleases=None): yield items whose version falls in the range; mirrorsSpecifierSet.filter, including PEP 440 pre-release buffering.Conversion back to SpecifierSet
to_specifier_set() -> SpecifierSet | None: a single SpecifierSet whoseto_range()yields the same range, orNoneif no such set exists. Redundant specifiers are dropped during the round-trip.to_specifier_sets() -> tuple[SpecifierSet, ...] | None: a tuple whose union equals the range, orNoneif no such tuple exists. Looser thanto_specifier_set(); useful for disjoint unions of intervals that no single SpecifierSet can express.Properties
is_empty:Truewhen no version satisfies the range.is_prerelease_only:Truewhen every match is a pre-release. Used bySpecifierSet.is_unsatisfiable.Standard protocol
__contains__,__bool__,__eq__,__hash__,__repr__, plus pickle support via__reduce__.Tests and docs
I've added full test coverage plus significant property-based testing. In particular I've added property-based testing of PubGrub invariants to ensure that this object can be used in PubGrub-like algorithms.