Skip to content

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

Open
veeceey wants to merge 2 commits intopypa:mainfrom
veeceey:fix/issue-810-specifier-gt-dev-local
Open

Fix > comparison for versions with dev+local segments#1097
veeceey wants to merge 2 commits intopypa:mainfrom
veeceey:fix/issue-810-specifier-gt-dev-local

Conversation

@veeceey
Copy link

@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
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
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).
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