@@ -1749,6 +1749,18 @@ impl PythonVariant {
17491749 }
17501750}
17511751impl PythonRequest {
1752+ /// Create a request from a `Requires-Python` constraint.
1753+ pub fn from_requires_python ( requires_python : RequiresPython ) -> Option < Self > {
1754+ if requires_python. is_unbounded ( ) {
1755+ return None ;
1756+ }
1757+
1758+ Some ( Self :: Version ( VersionRequest :: from_specifiers (
1759+ requires_python. into_specifiers ( ) ,
1760+ PythonVariant :: Default ,
1761+ ) ) )
1762+ }
1763+
17521764 /// Create a request from a string.
17531765 ///
17541766 /// This cannot fail, which means weird inputs will be parsed as [`PythonRequest::File`] or
@@ -2621,6 +2633,22 @@ impl fmt::Display for ExecutableName {
26212633}
26222634
26232635impl VersionRequest {
2636+ /// Create a [`VersionRequest`] from [`VersionSpecifiers`].
2637+ ///
2638+ /// If the specifiers consist of a single `==` constraint, the version is parsed as a
2639+ /// concrete version request (e.g., `MajorMinorPatch`) rather than a range. This ensures that
2640+ /// version-specific executable names (like `python3.12`) are included during discovery.
2641+ pub fn from_specifiers ( specifiers : VersionSpecifiers , variant : PythonVariant ) -> Self {
2642+ if let [ specifier] = specifiers. iter ( ) . as_slice ( ) {
2643+ if specifier. operator ( ) == & uv_pep440:: Operator :: Equal {
2644+ if let Ok ( request) = Self :: from_str ( & specifier. version ( ) . to_string ( ) ) {
2645+ return request;
2646+ }
2647+ }
2648+ }
2649+ Self :: Range ( specifiers, variant)
2650+ }
2651+
26242652 /// Drop any patch or prerelease information from the version request.
26252653 #[ must_use]
26262654 pub fn only_minor ( self ) -> Self {
@@ -3324,12 +3352,7 @@ fn parse_version_specifiers_request(
33243352 if specifiers. is_empty ( ) {
33253353 return Err ( Error :: InvalidVersionRequest ( s. to_string ( ) ) ) ;
33263354 }
3327- if let [ specifier] = specifiers. iter ( ) . as_slice ( ) {
3328- if specifier. operator ( ) == & uv_pep440:: Operator :: Equal {
3329- return VersionRequest :: from_str ( & specifier. version ( ) . to_string ( ) ) ;
3330- }
3331- }
3332- Ok ( VersionRequest :: Range ( specifiers, variant) )
3355+ Ok ( VersionRequest :: from_specifiers ( specifiers, variant) )
33333356}
33343357
33353358impl From < & PythonVersion > for VersionRequest {
@@ -4136,6 +4159,71 @@ mod tests {
41364159 VersionRequest :: from_str( "3.13tt" ) ,
41374160 Err ( Error :: InvalidVersionRequest ( _) )
41384161 ) ) ;
4162+
4163+ // `==` specifiers are parsed as concrete version requests via `from_specifiers`
4164+ assert_eq ! (
4165+ VersionRequest :: from_str( "==3.12" ) . unwrap( ) ,
4166+ VersionRequest :: MajorMinor ( 3 , 12 , PythonVariant :: Default )
4167+ ) ;
4168+ assert_eq ! (
4169+ VersionRequest :: from_str( "==3.12.1" ) . unwrap( ) ,
4170+ VersionRequest :: MajorMinorPatch ( 3 , 12 , 1 , PythonVariant :: Default )
4171+ ) ;
4172+ }
4173+
4174+ #[ test]
4175+ fn version_request_from_specifiers ( ) {
4176+ // A single `==` specifier is parsed as a concrete version request
4177+ assert_eq ! (
4178+ VersionRequest :: from_specifiers(
4179+ VersionSpecifiers :: from_str( "==3.12" ) . unwrap( ) ,
4180+ PythonVariant :: Default
4181+ ) ,
4182+ VersionRequest :: MajorMinor ( 3 , 12 , PythonVariant :: Default )
4183+ ) ;
4184+ assert_eq ! (
4185+ VersionRequest :: from_specifiers(
4186+ VersionSpecifiers :: from_str( "==3.12.1" ) . unwrap( ) ,
4187+ PythonVariant :: Default
4188+ ) ,
4189+ VersionRequest :: MajorMinorPatch ( 3 , 12 , 1 , PythonVariant :: Default )
4190+ ) ;
4191+
4192+ // Wildcard `==` specifiers remain as ranges
4193+ assert_eq ! (
4194+ VersionRequest :: from_specifiers(
4195+ VersionSpecifiers :: from_str( "==3.12.*" ) . unwrap( ) ,
4196+ PythonVariant :: Default
4197+ ) ,
4198+ VersionRequest :: Range (
4199+ VersionSpecifiers :: from_str( "==3.12.*" ) . unwrap( ) ,
4200+ PythonVariant :: Default
4201+ )
4202+ ) ;
4203+
4204+ // Range specifiers remain as ranges
4205+ assert_eq ! (
4206+ VersionRequest :: from_specifiers(
4207+ VersionSpecifiers :: from_str( ">=3.12" ) . unwrap( ) ,
4208+ PythonVariant :: Default
4209+ ) ,
4210+ VersionRequest :: Range (
4211+ VersionSpecifiers :: from_str( ">=3.12" ) . unwrap( ) ,
4212+ PythonVariant :: Default
4213+ )
4214+ ) ;
4215+
4216+ // Multi-specifier constraints remain as ranges
4217+ assert_eq ! (
4218+ VersionRequest :: from_specifiers(
4219+ VersionSpecifiers :: from_str( ">=3.12,<3.14" ) . unwrap( ) ,
4220+ PythonVariant :: Default
4221+ ) ,
4222+ VersionRequest :: Range (
4223+ VersionSpecifiers :: from_str( ">=3.12,<3.14" ) . unwrap( ) ,
4224+ PythonVariant :: Default
4225+ )
4226+ ) ;
41394227 }
41404228
41414229 #[ test]
0 commit comments