Skip to content

Implement is_unsatisfiable on SpecifierSet using ranges#1119

Merged
notatallshaw merged 22 commits intopypa:mainfrom
notatallshaw:is-unsatisfiable/is_unsatisfiable
Apr 12, 2026
Merged

Implement is_unsatisfiable on SpecifierSet using ranges#1119
notatallshaw merged 22 commits intopypa:mainfrom
notatallshaw:is-unsatisfiable/is_unsatisfiable

Conversation

@notatallshaw
Copy link
Copy Markdown
Member

Fixes #940
Fixes #306

This uses intervals that re-implement PEP 440 logic, the plan is to move the entire specifier machinery to use intervals, but that will be in a follow up PR. This approach focuses on relative simplicity, given that choice, and is not micro-optimized.

There are some edge cases around === that need to still be decided on, as this PR implements something closer to the specification than packaging quite implements (see #978 and #766), I will create a follow up PR to make that choice directly.

PR still needs a little clean up, so I will leave in draft for now and comment when ready for review.

@notatallshaw notatallshaw force-pushed the is-unsatisfiable/is_unsatisfiable branch 2 times, most recently from b67de8d to 7991a09 Compare March 27, 2026 01:28
@notatallshaw notatallshaw marked this pull request as ready for review March 27, 2026 01:31
@notatallshaw
Copy link
Copy Markdown
Member Author

I think this is ready for review, this attempts to be correct and implemented in an intuitive and as simple as possible way.

This does not intended to be performant, while not slow, there are a lot of opportunities for performance, but usually at the cost of adding complexity or making less intuitive .

@notatallshaw notatallshaw force-pushed the is-unsatisfiable/is_unsatisfiable branch 4 times, most recently from 4b067f1 to a810998 Compare March 27, 2026 13:44
Comment thread src/packaging/specifiers.py Outdated
Comment thread src/packaging/specifiers.py Outdated
Comment thread src/packaging/specifiers.py
@notatallshaw notatallshaw force-pushed the is-unsatisfiable/is_unsatisfiable branch from a810998 to 5f72426 Compare March 27, 2026 15:09
Comment thread src/packaging/specifiers.py Outdated
Comment thread src/packaging/specifiers.py Outdated
Comment thread src/packaging/specifiers.py Outdated
@notatallshaw notatallshaw force-pushed the is-unsatisfiable/is_unsatisfiable branch 3 times, most recently from c7f43ce to aa4b0ee Compare March 28, 2026 16:01
@notatallshaw

This comment was marked as outdated.

@notatallshaw notatallshaw force-pushed the is-unsatisfiable/is_unsatisfiable branch from aa4b0ee to 95694d5 Compare April 6, 2026 03:52
@henryiii henryiii mentioned this pull request Apr 7, 2026
@notatallshaw notatallshaw force-pushed the is-unsatisfiable/is_unsatisfiable branch 2 times, most recently from aad8702 to 37c6e22 Compare April 11, 2026 16:18
@notatallshaw notatallshaw marked this pull request as ready for review April 11, 2026 16:27
@notatallshaw
Copy link
Copy Markdown
Member Author

Okay, this is ready for review/approval again, apologies for having to make it draft after the last approval, but I've done the following:

  • Match behavior changes in Narrow >V post-release exclusion to match spec #1141 and Narrow <V.postN pre-release exclusion to match spec #1140 that were bugs discovered while trying to correctly model unsatisfiability
  • Support specifiers with prerelease=False not being satisfiable when only pre-releases would satisfy the specifier
  • Add property based testing that if the specifier set S1 is unsatisfiable then for any specifier S2 then S1 & S2 are unsatisfiable
  • Change terminology from "Specifier Interval" to "Version Range", I did a survey of other libraries and this was by far the most common term, and so likely what I am going to use for the eventual public API
  • Improve some of the doc strings

@notatallshaw notatallshaw force-pushed the is-unsatisfiable/is_unsatisfiable branch from 37c6e22 to 5c11f46 Compare April 11, 2026 16:43
@notatallshaw notatallshaw force-pushed the is-unsatisfiable/is_unsatisfiable branch from 5c11f46 to 94fa3b4 Compare April 11, 2026 16:47
@notatallshaw notatallshaw changed the title Implement is_unsatisfiable on SpecifierSet using intervals Implement is_unsatisfiable on SpecifierSet using ranges Apr 11, 2026
@henryiii henryiii requested a review from Copilot April 11, 2026 20:25
Copy link
Copy Markdown
Contributor

@henryiii henryiii left a comment

Choose a reason for hiding this comment

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

Not at a computer, I'll try to do a final review tonight. Triggered copilot just to see what it says.

Comment thread src/packaging/specifiers.py Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds a new SpecifierSet.is_unsatisfiable() API implemented via interval/range reasoning (an internal re-implementation of key PEP 440 ordering/containment rules), aimed at letting resolvers quickly detect contradictions without enumerating candidate versions.

Changes:

  • Implement interval-based range conversion for Specifier and range intersection logic for SpecifierSet, exposing SpecifierSet.is_unsatisfiable() with caching.
  • Add extensive unit tests for satisfiable/unsatisfiable specifier sets (including prerelease-mode variants) plus basic cache-behavior tests.
  • Add new Hypothesis property tests asserting soundness/monotonicity properties of is_unsatisfiable().

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/packaging/specifiers.py Adds internal boundary/range types, Specifier._to_ranges(), and SpecifierSet.is_unsatisfiable() based on intersected ranges + special casing for === and prerelease mode.
tests/test_specifiers.py Adds a large suite of concrete satisfiable/unsatisfiable cases (including prereleases=False scenarios) and caching assertions.
tests/property/test_specifier_extended.py Adds property-based tests for is_unsatisfiable() soundness and intersection monotonicity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/packaging/specifiers.py
Comment thread tests/test_specifiers.py
Comment thread src/packaging/specifiers.py Outdated
return self.version == other.version and self._kind == other._kind
return NotImplemented

def __lt__(self, other: object) -> bool:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not that important for an internal class, but __lt__ doesn't need to be object like __eq__ does, it can be more specific and then type checkers and IDEs can flag invalid comparisons. I'm guessing this is supposed to only support _BoundaryVersion | Version, given self._is_family(other), for example?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, fixed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What about the ones below?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah yeah, also fixed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If this was supposed to stay, I'd probably recommend putting all the new infrastructure into a new private file. But if a lot of it is going away in the next version, keeping it here is fine.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed this needs cleaning up, I'm committing to clean up all these over the next couple of PRs that will add internal usages and then a public API.

@notatallshaw notatallshaw force-pushed the is-unsatisfiable/is_unsatisfiable branch from 8e9d514 to 37dfc96 Compare April 12, 2026 05:45
@notatallshaw notatallshaw merged commit 3f4f5d4 into pypa:main Apr 12, 2026
57 checks passed
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.

Add an unsatisfiability check method to SpecifierSet Check for SpecifierSet syntactic consistency

3 participants