This is loosely a follow up to “Handling of Pre-Releases when backtracking” with a proposal that helps (but doesn’t solve) that situation, and in general makes it easier for Python package resolvers to agree on handling of multiple specifiers.
Prior Art
See dep-logic created by @frostming, and a discussion on the packaging github (that includes @pf_moore and @dstufft talking about some of the difficulties). As well as internal implementations in poetry-core, uv, and probably others I’m not aware of.
Goal
The goal of this proposal is to extend the existing specification (without contradicting it) by defining that for two version specifiers S1 and S2, there is an agreed intersection specifier S3.
If no versions satisfy the intersection of S1 and S2, they are considered “disjoint”.
This extension can be used during resolution to determine disjointness (and reject incompatible specifiers) or to simplify many specifiers into a single specifier.
Non-Goal
This is not intended to define what a Python resolver should do with the information. For example a Python resolver may choose to apply this at every resolution “round” to determine if two requirements are disjoint, but they may also choose to only apply it on the initial user requirements, etc.
Current Problems
Currently there are certain version specifiers that produce an “obvious” outcome when you take their union intersection, e.g. <=1.0.0
& <=2.0.0
= <=1.0.0
.
However the main difficulty is the moment you start including pre-releases, for example starting with a fairly non-controversial example <=1.0.0a1
& <=2.0.0
is intuitively <=1.0.0a1
. But if we were to look at the set of versions from each specifier, V1 from <=1.0.0a1
and V2 from <=2.0.0
, and took the intersection V1 & V2 then the specifier that produces this new version set is <1.0.0
.
Assuming the intuitive example is adopted <=1.0.0a1
and don’t worry about the set of versions, then you have lots of other examples that are tricky:
- Is
<=1.0.0a1
&<=2.0.0
the same as<=2.0.0
&<=1.0.0a1
? - Is
<=1.0.0
&<=2.0.0a1
equal to<=1.0.0
or<=1.0.0,prereleases=True
?
Proposal
I propose extending the specification where specifiers can be taken together for their “intersection” and a test for if they are “disjoint”. This would allow you to “simplify” a specifier set by taking the intersection of all of them to reduce to a single specifier.
To guide all of the answers to the tricky questions above I propose it is made clear that for 2 versions specifiers S1 and S2, then S1 & S2 = S2 & S1 (i.e specifiers under intersection are commutative) and that if S1 or S2 imply or explicitly include prereleases then S1 & S2 must include prereleases.
This would need to be expanded with lots of examples, but I think it is sufficient to answer all the questions about pre-releases. And then for when you don’t have questions about pre-rereleases S1 & S2 is the specifier that produces the version set V1 & V2, where V1 is the version set from S1, and V2 is the version set from V2. Again, a full spec would need to include lots of examples.
Open Question
If S1 and S2 are disjoint, should the result of S1 & S2 resolve to a standardized representation (e.g., EmptySpecifier as dep-logic does) or be None? Or should it be left to implementations to decide?
Next Steps
If there is consensus on this being a standard. I would be happy to draft a PEP, but I’m not familiar with that process. I assume I would need to find a sponsor?