Skip to content

馃悰 fix(spec): let ~=X.Y match later minor versions - #159

Merged
gaborbernat merged 3 commits into
tox-dev:mainfrom
darrenhuai:fix/compatible-release-upper-bound
Oct 27, 2026
Merged

gaborbernat merged 3 commits into
tox-dev:mainfrom
darrenhuai:fix/compatible-release-upper-bound

Conversation

@darrenhuai

@darrenhuai darrenhuai commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

get_interpreter("~=3.10") found nothing on a machine with 3.13 and 3.14 installed, while >=3.10,<4 found 3.14. 馃悰 PEP 440 defines ~=3.10 as >=3.10, ==3.*, and the docs list ~= as supported, so a two-part compatible release should accept every later 3.x. virtualenv -p "~=3.10", interpreter discovery, the uv store filter and PythonSpec.satisfies all shared the bug, because they all use the same specifier check.

The version parser pads every release to three parts, and the compatible-release check derived its upper bound from that padded form, so ~=3.10 behaved like ~=3.10.0. The check now compares the release prefix the spec wrote, all parts but the last, the way ==X.* wildcards already match. Comparing the prefix also stops ~=3.12.0 from accepting 3.13.0a1, which used to slip under the computed bound.

鈿狅笍 Users who wrote ~=3.12 to mean "3.12 only" now get later 3.x interpreters too; PEP 440 spells that ~=3.12.0 or ==3.12.*. A bare ~=3, which PEP 440 rejects, stays accepted and means ==3.*, matching what interpreter discovery already did with it.

darrenhuai and others added 3 commits October 21, 2026 23:53
SimpleVersion pads every release to three parts, and the compatible
release check built its upper bound by dropping the last part of that
padded tuple. That is only right when the specifier itself has three
parts. For ~=3.10 it gave >=3.10,<3.11 instead of the >=3.10,==3.* that
PEP 440 defines, so get_interpreter("~=3.10") returned None on a machine
whose Pythons are all 3.11 or newer, and the uv store filter and
PythonSpec.satisfies with a full X.Y.Z spec skipped the same
interpreters. virtualenv -p "~=3.10" fails the same way.

The check now compares every release part the specifier wrote except
the last, the same prefix match the wildcard check uses. Comparing the
prefix also stops ~=3.12.0 from accepting 3.13.0a1, which sorted below
the old 3.13 bound. A bare ~=3 is not valid PEP 440, but it was
accepted before, so it keeps working and now means ==3.* in both
SimpleSpecifier.contains and PythonInfo.satisfies, which used to
disagree on it.

Anyone who used ~=3.12 to mean 3.12 only now gets later 3.x releases
too; PEP 440 spells that ~=3.12.0 or ==3.12.*.
@gaborbernat gaborbernat added the bug Something isn't working label Oct 27, 2026
@gaborbernat gaborbernat changed the title fix(spec): let ~=X.Y match later minor versions 馃悰 fix(spec): let ~=X.Y match later minor versions Oct 27, 2026

@gaborbernat gaborbernat left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@gaborbernat
gaborbernat merged commit 27f51c2 into tox-dev:main Oct 27, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants