fix: match version specs with compiler pragma semantics#7
Merged
Conversation
Source-version inference matched pragma specs with raw PEP 440 ordering comparisons, so caret ranges like ^0.4.2 (upper bound <0.5.0) admitted the 0.5.0 alphas, which sort below 0.5.0. Source validation then picked the prerelease target as the source compiler and the compile failed with a VersionException, since Vyper itself checks pragmas with SpecifierSet semantics that exclude prereleases of an exclusive bound. Match candidate versions the way vyper.ast.pre_parser does: normalize bare versions to exact pins and npm-style carets to PEP 440 compatible-release clauses, then test with SpecifierSet.contains(..., prereleases=True). The clause-based matcher remains as a fallback for specs SpecifierSet cannot parse. Fixes #6 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JtAsisni8yijKyn2e29q1R
known_versions_satisfying() now evaluates valid pragmas through packaging's SpecifierSet, but compiler_version_for_spec() still decided whether to choose the lowest or newest matching compiler with the legacy regex parser. That parser only recognizes full X.Y.Z versions, so valid PEP 440 compatible-release specs such as ~=0.4 were treated as if they had no lower bound.
The result was that compiler_version_for_spec("~=0.4") selected the newest known matching compiler, currently 0.5.0a3, instead of the source floor 0.4.0. That was inconsistent with the new SpecifierSet-based matching path and could reintroduce prerelease target selection for valid compiler-style pragmas.
Teach _has_lower_bound() to inspect SpecifierSet specifiers when the pragma parses as PEP 440. Compatible-release, greater-than, and greater-or-equal operators are lower bounds; exact pins are lower bounds unless they are wildcard pins, preserving the existing behavior of selecting the newest compiler for ==0.4.*.
Add regressions for ~=0.4 and ~=0.4.2 selecting their source floors, plus ==0.4.* continuing to select the newest patch in that wildcard range.
banteg
approved these changes
Jul 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6.
Problem
Source-version inference selected the prerelease target
0.5.0a3as the source validation compiler for a^0.4.2pragma, so the source compile failed with:Root cause
known_versions_satisfying()expanded^0.4.2to>=0.4.2, <0.5.0and evaluated the clauses with raw PEP 440 ordering comparisons. Under PEP 440 ordering,0.5.0a3 < 0.5.0is true, so the0.5.0alphas leaked through the caret's upper bound andcompiler_version_for_source_validation()picked the newest one.Vyper itself checks pragmas differently (
vyper.ast.pre_parser.validate_version_pragma): it normalizes^X.Y.Zto~=X.Y.Zand bare versions to==X.Y.Z, then testsSpecifierSet.contains(compiler_version, prereleases=True). PEP 440 specifier semantics reject a prerelease at an exclusive ordered bound (<0.5.0never admits0.5.0a3), so vyupgrade's matcher was strictly more permissive than the compiler it invokes.Fix
known_versions_satisfying()now mirrors the compiler's check exactly: the spec is normalized the same way and candidates are tested withSpecifierSet.contains(..., prereleases=True). The old clause-based matcher is kept only as a fallback for specsSpecifierSetcannot parse. With this, membership in the candidate list is equivalent to "this compiler accepts this pragma", so this class of divergence cannot recur.For the issue's repro, source validation now picks
0.4.3:This also corrects
compiler_version_for_spec("<0.5.0"), which previously resolved to0.5.0a3(same bug class).Test updates
Two existing assertions used
>=0.5.0a1,<0.5.0as a broad alpha pragma, but the real compiler rejects that spec (0.5.0aNis a prerelease of the excluded bound0.5.0— verified against vyper 0.5.0a3). They now use>=0.5.0a1,<0.6.0, the form the CLI fixtures already use, plus an explicit assertion that>=0.5.0a1,<0.5.0matches nothing. A new regression test covers the^0.4.2/0.5.0a3scenario and legacy^0.1.0b14caret pragmas.ruff check, the full unit suite (484 passed), andtests/test_cli_integration.py(real compilers) all pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01JtAsisni8yijKyn2e29q1R
Generated by Claude Code