Skip to content

Commit 2efecda

Browse files
zaniebclaude
andauthored
Allow pre-release Python requests with non-zero patch versions (#19286)
Closes #19283 Closes #19277 Closes #19285 --------- Co-authored-by: Claude <[email protected]>
1 parent e5d20c6 commit 2efecda

2 files changed

Lines changed: 146 additions & 21 deletions

File tree

crates/uv-python/src/discovery.rs

Lines changed: 135 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ pub enum VersionRequest {
203203
MajorMinor(u8, u8, PythonVariant),
204204
MajorMinorPatch(u8, u8, u8, PythonVariant),
205205
MajorMinorPrerelease(u8, u8, Prerelease, PythonVariant),
206+
MajorMinorPatchPrerelease(u8, u8, u8, Prerelease, PythonVariant),
206207
Range(VersionSpecifiers, PythonVariant),
207208
}
208209

@@ -729,7 +730,8 @@ fn find_all_minor(
729730
}
730731
VersionRequest::MajorMinor(_, _, _)
731732
| VersionRequest::MajorMinorPatch(_, _, _, _)
732-
| VersionRequest::MajorMinorPrerelease(_, _, _, _) => Either::Right(iter::empty()),
733+
| VersionRequest::MajorMinorPrerelease(_, _, _, _)
734+
| VersionRequest::MajorMinorPatchPrerelease(_, _, _, _, _) => Either::Right(iter::empty()),
733735
}
734736
}
735737

@@ -2669,7 +2671,8 @@ impl VersionRequest {
26692671
Self::Major(..) => self,
26702672
Self::MajorMinor(..) => self,
26712673
Self::MajorMinorPatch(major, minor, _, variant)
2672-
| Self::MajorMinorPrerelease(major, minor, _, variant) => {
2674+
| Self::MajorMinorPrerelease(major, minor, _, variant)
2675+
| Self::MajorMinorPatchPrerelease(major, minor, _, _, variant) => {
26732676
Self::MajorMinor(major, minor, variant)
26742677
}
26752678
}
@@ -2680,11 +2683,13 @@ impl VersionRequest {
26802683
&self,
26812684
implementation: Option<&ImplementationName>,
26822685
) -> Vec<ExecutableName> {
2683-
let prerelease = if let Self::MajorMinorPrerelease(_, _, prerelease, _) = self {
2684-
// Include the prerelease version, e.g., `python3.8a`
2685-
Some(prerelease)
2686-
} else {
2687-
None
2686+
let prerelease = match self {
2687+
Self::MajorMinorPrerelease(_, _, prerelease, _)
2688+
| Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => {
2689+
// Include the prerelease version, e.g., `python3.8a`
2690+
Some(prerelease)
2691+
}
2692+
_ => None,
26882693
};
26892694

26902695
// Push a default one
@@ -2772,6 +2777,7 @@ impl VersionRequest {
27722777
Self::MajorMinor(major, _, _) => Some(*major),
27732778
Self::MajorMinorPatch(major, _, _, _) => Some(*major),
27742779
Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2780+
Self::MajorMinorPatchPrerelease(major, _, _, _, _) => Some(*major),
27752781
}
27762782
}
27772783

@@ -2783,6 +2789,7 @@ impl VersionRequest {
27832789
Self::MajorMinor(_, minor, _) => Some(*minor),
27842790
Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
27852791
Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2792+
Self::MajorMinorPatchPrerelease(_, minor, _, _, _) => Some(*minor),
27862793
}
27872794
}
27882795

@@ -2794,6 +2801,7 @@ impl VersionRequest {
27942801
Self::MajorMinor(_, _, _) => None,
27952802
Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
27962803
Self::MajorMinorPrerelease(_, _, _, _) => None,
2804+
Self::MajorMinorPatchPrerelease(_, _, patch, _, _) => Some(*patch),
27972805
}
27982806
}
27992807

@@ -2805,6 +2813,7 @@ impl VersionRequest {
28052813
Self::MajorMinor(_, _, _) => None,
28062814
Self::MajorMinorPatch(_, _, _, _) => None,
28072815
Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
2816+
Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => Some(prerelease),
28082817
}
28092818
}
28102819

@@ -2842,6 +2851,13 @@ impl VersionRequest {
28422851
));
28432852
}
28442853
}
2854+
Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
2855+
if (*major, *minor) < (3, 6) {
2856+
return Err(format!(
2857+
"Python <3.6 is not supported but {major}.{minor}.{patch}{prerelease} was requested."
2858+
));
2859+
}
2860+
}
28452861
// TODO(zanieb): We could do some checking here to see if the range can be satisfied
28462862
Self::Range(_, _) => (),
28472863
}
@@ -2933,6 +2949,19 @@ impl VersionRequest {
29332949
) == (*major, *minor, *prerelease)
29342950
&& variant.matches_interpreter(interpreter)
29352951
}
2952+
Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
2953+
let version = interpreter.python_version();
2954+
let Some(interpreter_prerelease) = version.pre() else {
2955+
return false;
2956+
};
2957+
(
2958+
interpreter.python_major(),
2959+
interpreter.python_minor(),
2960+
interpreter.python_patch(),
2961+
interpreter_prerelease,
2962+
) == (*major, *minor, *patch, *prerelease)
2963+
&& variant.matches_interpreter(interpreter)
2964+
}
29362965
}
29372966
}
29382967

@@ -2968,6 +2997,14 @@ impl VersionRequest {
29682997
(version.major(), version.minor(), version.pre())
29692998
== (*major, *minor, Some(*prerelease))
29702999
}
3000+
Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3001+
(
3002+
version.major(),
3003+
version.minor(),
3004+
version.patch(),
3005+
version.pre(),
3006+
) == (*major, *minor, Some(*patch), Some(*prerelease))
3007+
}
29713008
}
29723009
}
29733010

@@ -3007,6 +3044,9 @@ impl VersionRequest {
30073044
Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
30083045
(*self_major, *self_minor) == (major, minor)
30093046
}
3047+
Self::MajorMinorPatchPrerelease(self_major, self_minor, _, _, _) => {
3048+
(*self_major, *self_minor) == (major, minor)
3049+
}
30103050
}
30113051
}
30123052

@@ -3039,10 +3079,24 @@ impl VersionRequest {
30393079
.with_pre(prerelease),
30403080
),
30413081
Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3042-
// Pre-releases of Python versions are always for the zero patch version
3082+
// Pre-releases without a patch in the request match the zero patch version
30433083
(*self_major, *self_minor, 0, Some(*self_prerelease))
30443084
== (major, minor, patch, prerelease)
30453085
}
3086+
Self::MajorMinorPatchPrerelease(
3087+
self_major,
3088+
self_minor,
3089+
self_patch,
3090+
self_prerelease,
3091+
_,
3092+
) => {
3093+
(
3094+
*self_major,
3095+
*self_minor,
3096+
*self_patch,
3097+
Some(*self_prerelease),
3098+
) == (major, minor, patch, prerelease)
3099+
}
30463100
}
30473101
}
30483102

@@ -3062,6 +3116,7 @@ impl VersionRequest {
30623116
Self::MajorMinor(..) => false,
30633117
Self::MajorMinorPatch(..) => true,
30643118
Self::MajorMinorPrerelease(..) => false,
3119+
Self::MajorMinorPatchPrerelease(..) => true,
30653120
Self::Range(_, _) => false,
30663121
}
30673122
}
@@ -3082,6 +3137,9 @@ impl VersionRequest {
30823137
Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
30833138
Self::MajorMinorPrerelease(major, minor, prerelease, variant)
30843139
}
3140+
Self::MajorMinorPatchPrerelease(major, minor, _, prerelease, variant) => {
3141+
Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3142+
}
30853143
Self::Range(_, _) => self,
30863144
}
30873145
}
@@ -3095,6 +3153,7 @@ impl VersionRequest {
30953153
Self::MajorMinor(..) => false,
30963154
Self::MajorMinorPatch(..) => false,
30973155
Self::MajorMinorPrerelease(..) => true,
3156+
Self::MajorMinorPatchPrerelease(..) => true,
30983157
Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
30993158
}
31003159
}
@@ -3107,6 +3166,7 @@ impl VersionRequest {
31073166
| Self::MajorMinor(_, _, variant)
31083167
| Self::MajorMinorPatch(_, _, _, variant)
31093168
| Self::MajorMinorPrerelease(_, _, _, variant)
3169+
| Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
31103170
| Self::Range(_, variant) => variant.is_debug(),
31113171
}
31123172
}
@@ -3119,6 +3179,7 @@ impl VersionRequest {
31193179
| Self::MajorMinor(_, _, variant)
31203180
| Self::MajorMinorPatch(_, _, _, variant)
31213181
| Self::MajorMinorPrerelease(_, _, _, variant)
3182+
| Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
31223183
| Self::Range(_, variant) => variant.is_freethreaded(),
31233184
}
31243185
}
@@ -3142,6 +3203,15 @@ impl VersionRequest {
31423203
Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
31433204
Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Default)
31443205
}
3206+
Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3207+
Self::MajorMinorPatchPrerelease(
3208+
major,
3209+
minor,
3210+
patch,
3211+
prerelease,
3212+
PythonVariant::Default,
3213+
)
3214+
}
31453215
Self::Range(specifiers, _) => Self::Range(specifiers, PythonVariant::Default),
31463216
}
31473217
}
@@ -3155,6 +3225,7 @@ impl VersionRequest {
31553225
| Self::MajorMinor(_, _, variant)
31563226
| Self::MajorMinorPatch(_, _, _, variant)
31573227
| Self::MajorMinorPrerelease(_, _, _, variant)
3228+
| Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
31583229
| Self::Range(_, variant) => Some(*variant),
31593230
}
31603231
}
@@ -3174,10 +3245,14 @@ impl VersionRequest {
31743245
u64::from(*minor),
31753246
u64::from(*patch),
31763247
])),
3177-
// Pre-releases of Python versions are always for the zero patch version
3248+
// Pre-releases without a patch use the zero patch version
31783249
Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
31793250
Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
31803251
),
3252+
Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => Some(
3253+
Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3254+
.with_pre(Some(*prerelease)),
3255+
),
31813256
}
31823257
}
31833258

@@ -3209,6 +3284,12 @@ impl VersionRequest {
32093284
.with_pre(Some(*prerelease)),
32103285
)))
32113286
}
3287+
Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3288+
Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3289+
Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3290+
.with_pre(Some(*prerelease)),
3291+
)))
3292+
}
32123293
Self::Range(specifiers, _) => Some(specifiers.clone()),
32133294
}
32143295
}
@@ -3298,16 +3379,16 @@ impl FromStr for VersionRequest {
32983379
}
32993380
Ok(Self::MajorMinor(*major, *minor, variant))
33003381
}
3301-
// e.g. `3.12.1` or `3.13.0rc1`
3382+
// e.g. `3.12.1`, `3.13.0rc1`, or `3.14.5rc1`
33023383
[major, minor, patch] => {
33033384
if let Some(prerelease) = prerelease {
3304-
// Prereleases are only allowed for the first patch version, e.g, 3.12.2rc1
3305-
// isn't a proper Python release
3306-
if *patch != 0 {
3307-
return Err(Error::InvalidVersionRequest(s.to_string()));
3385+
if *patch == 0 {
3386+
return Ok(Self::MajorMinorPrerelease(
3387+
*major, *minor, prerelease, variant,
3388+
));
33083389
}
3309-
return Ok(Self::MajorMinorPrerelease(
3310-
*major, *minor, prerelease, variant,
3390+
return Ok(Self::MajorMinorPatchPrerelease(
3391+
*major, *minor, *patch, prerelease, variant,
33113392
));
33123393
}
33133394
Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
@@ -3381,6 +3462,13 @@ impl fmt::Display for VersionRequest {
33813462
Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
33823463
write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
33833464
}
3465+
Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3466+
write!(
3467+
f,
3468+
"{major}.{minor}.{patch}{prerelease}{}",
3469+
variant.display_suffix()
3470+
)
3471+
}
33843472
Self::Range(specifiers, _) => write!(f, "{specifiers}"),
33853473
}
33863474
}
@@ -3940,6 +4028,12 @@ mod tests {
39404028
"3.13rc4"
39414029
);
39424030

4031+
assert_eq!(
4032+
PythonRequest::Version(VersionRequest::from_str("3.14.5rc1").unwrap())
4033+
.to_canonical_string(),
4034+
"3.14.5rc1"
4035+
);
4036+
39434037
assert_eq!(
39444038
PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
39454039
"foo"
@@ -4091,12 +4185,32 @@ mod tests {
40914185
),
40924186
"Pre-release version requests require a minor version"
40934187
);
4094-
assert!(
4095-
matches!(
4096-
VersionRequest::from_str("3.13.2rc1"),
4097-
Err(Error::InvalidVersionRequest(_))
4188+
assert_eq!(
4189+
VersionRequest::from_str("3.14.5rc1").unwrap(),
4190+
VersionRequest::MajorMinorPatchPrerelease(
4191+
3,
4192+
14,
4193+
5,
4194+
Prerelease {
4195+
kind: PrereleaseKind::Rc,
4196+
number: 1
4197+
},
4198+
PythonVariant::Default
40984199
),
4099-
"Pre-release version requests require a patch version of zero"
4200+
"Pre-release version requests with a non-zero patch are allowed (e.g., `3.14.5rc1`)"
4201+
);
4202+
assert_eq!(
4203+
VersionRequest::from_str("3.13.2rc1").unwrap(),
4204+
VersionRequest::MajorMinorPatchPrerelease(
4205+
3,
4206+
13,
4207+
2,
4208+
Prerelease {
4209+
kind: PrereleaseKind::Rc,
4210+
number: 1
4211+
},
4212+
PythonVariant::Default
4213+
)
41004214
);
41014215
assert!(
41024216
matches!(

crates/uv/tests/it/python_install.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2620,6 +2620,17 @@ fn python_install_prerelease() {
26202620
Installed Python 3.15.0a2 in [TIME]
26212621
+ cpython-3.15.0a2-[PLATFORM]
26222622
");
2623+
2624+
// Install a release candidate for a non-zero patch version
2625+
uv_snapshot!(context.filters(), context.python_install().arg("3.14.5rc1"), @"
2626+
success: true
2627+
exit_code: 0
2628+
----- stdout -----
2629+
2630+
----- stderr -----
2631+
Installed Python 3.14.5rc1 in [TIME]
2632+
+ cpython-3.14.5rc1-[PLATFORM] (python3.14)
2633+
");
26232634
}
26242635

26252636
#[test]

0 commit comments

Comments
 (0)