Skip to content

Fix > comparison for versions with dev+local segments#1097

Merged
notatallshaw merged 4 commits intopypa:mainfrom
veeceey:fix/issue-810-specifier-gt-dev-local
Feb 28, 2026
Merged

Fix > comparison for versions with dev+local segments#1097
notatallshaw merged 4 commits intopypa:mainfrom
veeceey:fix/issue-810-specifier-gt-dev-local

Conversation

@veeceey
Copy link
Copy Markdown
Contributor

@veeceey veeceey commented Feb 23, 2026

The > specifier was incorrectly rejecting versions like 4.1.0a2.dev1235+local against >4.1.0a2.dev1234.

The root cause is the local version guard in _compare_greater_than — it used _base_version() to check if a local version matches the spec version, but _base_version strips pre/dev/post segments too aggressively. This made 4.1.0a2.dev1235 and 4.1.0a2.dev1234 appear as the same base version (4.1.0), so any local variant got rejected.

Switched to _public_version() which only strips the local segment. Now the comparison correctly identifies that 4.1.0a2.dev1235 != 4.1.0a2.dev1234, so 4.1.0a2.dev1235+local properly passes the >4.1.0a2.dev1234 check.

>>> from packaging.specifiers import Specifier
>>> spec = Specifier(">4.1.0a2.dev1234")
>>> spec.contains("4.1.0a2.dev1235+local", prereleases=True)
True  # was incorrectly False before
>>> spec.contains("4.1.0a2.dev1234+local", prereleases=True)
False  # still correctly False (same public version)

Fixes #810

The greater-than specifier incorrectly rejected versions like
4.1.0a2.dev1235+local against >4.1.0a2.dev1234. The local version
guard used _base_version which strips pre/dev/post segments, making
different dev versions appear equal. Replaced with _public_version
which only strips the local segment, correctly identifying when a
local version genuinely differs from the spec version.

Fixes pypa#810
@veeceey
Copy link
Copy Markdown
Contributor Author

veeceey commented Feb 23, 2026

Manual test results:

$ PYTHONPATH=src python3 -c "
from packaging.specifiers import Specifier
spec = Specifier('>4.1.0a2.dev1234')

# Bug scenario from issue
print('dev1234:', spec.contains('4.1.0a2.dev1234', prereleases=True))       # False ✓
print('dev1235:', spec.contains('4.1.0a2.dev1235', prereleases=True))       # True ✓
print('dev1234+local:', spec.contains('4.1.0a2.dev1234+local', prereleases=True))  # False ✓
print('dev1235+local:', spec.contains('4.1.0a2.dev1235+local', prereleases=True))  # True ✓ (was False)

# Edge cases
spec2 = Specifier('>1.0')
print('1.0+local vs >1.0:', spec2.contains('1.0+local'))     # False ✓
print('1.1+local vs >1.0:', spec2.contains('1.1+local'))     # True ✓
"

dev1234: False
dev1235: True
dev1234+local: False
dev1235+local: True
1.0+local vs >1.0: False
1.1+local vs >1.0: True

All 1216 specifier tests pass.

@notatallshaw
Copy link
Copy Markdown
Member

notatallshaw commented Feb 23, 2026

I would expect additional behavior change from changing _base_version to _public_version, but I've not carefully reviewed this yet, if so could you add additional tests that would be impacted by this, such as dev, pre, post, local segments in either or both the specifier and the version.

Cover the behavior change from _base_version to _public_version by
testing that local versions with varying pre, dev, and post segments
are correctly accepted or rejected by the > operator.

True cases: local versions whose public part genuinely exceeds the spec
(e.g. 1.0a2+local > 1.0a1, 1.0.dev2+local > 1.0.dev1).

False cases: local versions whose public part matches the spec exactly
(e.g. 1.0a1+local should not match >1.0a1).
Copy link
Copy Markdown
Member

@notatallshaw notatallshaw left a comment

Choose a reason for hiding this comment

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

Overall this is good and achieves greater spec compliance, just need addressing a couple of items I have outlined.

Comment thread src/packaging/specifiers.py Outdated
Comment thread src/packaging/specifiers.py Outdated
veeceey and others added 2 commits February 27, 2026 22:11
- Replace _public_version(spec) with just spec, since a > specifier
  can never contain a local segment
- Rewrite comment to quote the PEP spec exactly
@notatallshaw notatallshaw merged commit 16f5587 into pypa:main Feb 28, 2026
50 checks passed
ngoldbaum pushed a commit to ngoldbaum/packaging that referenced this pull request Apr 1, 2026
* Fix > comparison for versions with dev+local segments

The greater-than specifier incorrectly rejected versions like
4.1.0a2.dev1235+local against >4.1.0a2.dev1234. The local version
guard used _base_version which strips pre/dev/post segments, making
different dev versions appear equal. Replaced with _public_version
which only strips the local segment, correctly identifying when a
local version genuinely differs from the spec version.

Fixes pypa#810

* Add tests for > specifier with local+pre/dev/post segments

Cover the behavior change from _base_version to _public_version by
testing that local versions with varying pre, dev, and post segments
are correctly accepted or rejected by the > operator.

True cases: local versions whose public part genuinely exceeds the spec
(e.g. 1.0a2+local > 1.0a1, 1.0.dev2+local > 1.0.dev1).

False cases: local versions whose public part matches the spec exactly
(e.g. 1.0a1+local should not match >1.0a1).

* Address review: simplify local version guard and improve comment

- Replace _public_version(spec) with just spec, since a > specifier
  can never contain a local segment
- Rewrite comment to quote the PEP spec exactly

---------

Co-authored-by: Damian Shaw <[email protected]>
This was referenced Apr 13, 2026
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.

Specifier Greater than comparison returns incorrect result for a version with dev+local parts

2 participants