Skip to content

Commit 1db4861

Browse files
gaborbernatclaude
andcommitted
🐛 fix(discovery): match prerelease versions against major.minor specs
Python prerelease versions like 3.15.0a6 were incorrectly failing to match version specifiers like >=3.15. This broke testing of prereleases in Fedora and other environments that build libraries against alpha/beta Python versions. The root cause was always comparing the full version string "3.15.0a6" against the normalized specifier version "3.15.0". Since PEP 440 defines prereleases as strictly less than final releases, this comparison failed even though the intent of >=3.15 is to match any 3.15.x version including prereleases. The fix matches the comparison precision to the specifier precision. For a two-component specifier like >=3.15, we now compare "3.15" without prerelease suffix, allowing prereleases to satisfy the constraint. Prerelease suffixes are only included when the specifier itself contains a prerelease marker. Fixes #45 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c4ec5ca commit 1db4861

2 files changed

Lines changed: 22 additions & 5 deletions

File tree

src/python_discovery/_py_info.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -469,12 +469,22 @@ def _satisfies_version_specifier(self, spec: PythonSpec) -> bool:
469469
if spec.version_specifier is None: # pragma: no cover
470470
return True
471471
version_info = self.version_info
472-
release = f"{version_info.major}.{version_info.minor}.{version_info.micro}"
473-
if version_info.releaselevel != "final":
474-
suffix = {"alpha": "a", "beta": "b", "candidate": "rc"}.get(version_info.releaselevel)
475-
if suffix is not None: # pragma: no branch # releaselevel is always alpha/beta/candidate here
472+
for specifier in spec.version_specifier:
473+
if specifier.version is None:
474+
continue
475+
release = ".".join(
476+
str(c)
477+
for c in [version_info.major, version_info.minor, version_info.micro][: len(specifier.version.release)]
478+
)
479+
if (
480+
specifier.version.pre_type is not None
481+
and version_info.releaselevel != "final"
482+
and (suffix := {"alpha": "a", "beta": "b", "candidate": "rc"}.get(version_info.releaselevel))
483+
):
476484
release = f"{release}{suffix}{version_info.serial}"
477-
return spec.version_specifier.contains(release)
485+
if not specifier.contains(release):
486+
return False
487+
return True
478488

479489
_current_system = None
480490
_current = None

tests/test_py_info_extra.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,13 @@ def test_satisfies_prerelease_candidate() -> None:
297297
assert info.satisfies(spec, impl_must_match=False) is True
298298

299299

300+
def test_satisfies_prerelease_with_major_minor_spec() -> None:
301+
info = copy.deepcopy(CURRENT)
302+
info.version_info = VersionInfo(3, 15, 0, "alpha", 6)
303+
spec = PythonSpec.from_string_spec(">=3.15")
304+
assert info.satisfies(spec, impl_must_match=False) is True
305+
306+
300307
def test_satisfies_path_not_abs_basename_match() -> None:
301308
info = copy.deepcopy(CURRENT)
302309
basename = Path(info.original_executable).stem

0 commit comments

Comments
 (0)