Skip to content

Commit a68146d

Browse files
zaniebBurntSushi
andauthored
Support toolchain requests with platform-tag style Python implementations and version (#4407)
Closes #4399 --------- Co-authored-by: Andrew Gallant <[email protected]>
1 parent e5f061e commit a68146d

File tree

2 files changed

+87
-15
lines changed

2 files changed

+87
-15
lines changed

crates/uv-toolchain/src/discovery.rs

+80-9
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ pub enum ToolchainRequest {
4444
File(PathBuf),
4545
/// The name of a Python executable (i.e. for lookup in the PATH) e.g. `foopython3`
4646
ExecutableName(String),
47-
/// A Python implementation without a version e.g. `pypy`
47+
/// A Python implementation without a version e.g. `pypy` or `pp`
4848
Implementation(ImplementationName),
49-
/// A Python implementation name and version e.g. `pypy3.8` or `[email protected]`
49+
/// A Python implementation name and version e.g. `pypy3.8` or `[email protected]` or `pp38`
5050
ImplementationVersion(ImplementationName, VersionRequest),
5151
/// A request for a specific toolchain key e.g. `cpython-3.12-x86_64-linux-gnu`
5252
/// Generally these refer to uv-managed toolchain downloads.
@@ -919,7 +919,7 @@ impl ToolchainRequest {
919919
///
920920
/// This cannot fail, which means weird inputs will be parsed as [`ToolchainRequest::File`] or [`ToolchainRequest::ExecutableName`].
921921
pub fn parse(value: &str) -> Self {
922-
// e.g. `3.12.1`
922+
// e.g. `3.12.1`, `312`, or `>=3.12`
923923
if let Ok(version) = VersionRequest::from_str(value) {
924924
return Self::Version(version);
925925
}
@@ -937,18 +937,25 @@ impl ToolchainRequest {
937937
}
938938
}
939939
}
940-
for implementation in ImplementationName::iter() {
940+
for implementation in ImplementationName::possible_names() {
941941
if let Some(remainder) = value
942942
.to_ascii_lowercase()
943943
.strip_prefix(Into::<&str>::into(implementation))
944944
{
945945
// e.g. `pypy`
946946
if remainder.is_empty() {
947-
return Self::Implementation(*implementation);
947+
return Self::Implementation(
948+
// Safety: The name matched the possible names above
949+
ImplementationName::from_str(implementation).unwrap(),
950+
);
948951
}
949-
// e.g. `pypy3.12`
952+
// e.g. `pypy3.12` or `pp312`
950953
if let Ok(version) = VersionRequest::from_str(remainder) {
951-
return Self::ImplementationVersion(*implementation, version);
954+
return Self::ImplementationVersion(
955+
// Safety: The name matched the possible names above
956+
ImplementationName::from_str(implementation).unwrap(),
957+
version,
958+
);
952959
}
953960
}
954961
}
@@ -1267,8 +1274,22 @@ impl FromStr for VersionRequest {
12671274
type Err = Error;
12681275

12691276
fn from_str(s: &str) -> Result<Self, Self::Err> {
1277+
fn parse_nosep(s: &str) -> Option<VersionRequest> {
1278+
let mut chars = s.chars();
1279+
let major = chars.next()?.to_digit(10)?.try_into().ok()?;
1280+
if chars.as_str().is_empty() {
1281+
return Some(VersionRequest::Major(major));
1282+
}
1283+
let minor = chars.as_str().parse::<u8>().ok()?;
1284+
Some(VersionRequest::MajorMinor(major, minor))
1285+
}
1286+
1287+
// e.g. `3`, `38`, `312`
1288+
if let Some(request) = parse_nosep(s) {
1289+
Ok(request)
1290+
}
12701291
// e.g. `3.12.1`
1271-
if let Ok(versions) = s
1292+
else if let Ok(versions) = s
12721293
.splitn(3, '.')
12731294
.map(str::parse::<u8>)
12741295
.collect::<Result<Vec<_>, _>>()
@@ -1284,6 +1305,7 @@ impl FromStr for VersionRequest {
12841305
};
12851306

12861307
Ok(selector)
1308+
12871309
// e.g. `>=3.12.1,<3.12`
12881310
} else if let Ok(specifiers) = VersionSpecifiers::from_str(s) {
12891311
if specifiers.is_empty() {
@@ -1549,6 +1571,7 @@ mod tests {
15491571
use assert_fs::{prelude::*, TempDir};
15501572
use test_log::test;
15511573

1574+
use super::Error;
15521575
use crate::{
15531576
discovery::{ToolchainRequest, VersionRequest},
15541577
implementation::ImplementationName,
@@ -1587,13 +1610,35 @@ mod tests {
15871610
ToolchainRequest::parse("pypy"),
15881611
ToolchainRequest::Implementation(ImplementationName::PyPy)
15891612
);
1613+
assert_eq!(
1614+
ToolchainRequest::parse("pp"),
1615+
ToolchainRequest::Implementation(ImplementationName::PyPy)
1616+
);
1617+
assert_eq!(
1618+
ToolchainRequest::parse("cp"),
1619+
ToolchainRequest::Implementation(ImplementationName::CPython)
1620+
);
15901621
assert_eq!(
15911622
ToolchainRequest::parse("pypy3.10"),
15921623
ToolchainRequest::ImplementationVersion(
15931624
ImplementationName::PyPy,
15941625
VersionRequest::from_str("3.10").unwrap()
15951626
)
15961627
);
1628+
assert_eq!(
1629+
ToolchainRequest::parse("pp310"),
1630+
ToolchainRequest::ImplementationVersion(
1631+
ImplementationName::PyPy,
1632+
VersionRequest::from_str("3.10").unwrap()
1633+
)
1634+
);
1635+
assert_eq!(
1636+
ToolchainRequest::parse("cp38"),
1637+
ToolchainRequest::ImplementationVersion(
1638+
ImplementationName::CPython,
1639+
VersionRequest::from_str("3.8").unwrap()
1640+
)
1641+
);
15971642
assert_eq!(
15981643
ToolchainRequest::parse("[email protected]"),
15991644
ToolchainRequest::ImplementationVersion(
@@ -1603,7 +1648,10 @@ mod tests {
16031648
);
16041649
assert_eq!(
16051650
ToolchainRequest::parse("pypy310"),
1606-
ToolchainRequest::ExecutableName("pypy310".to_string())
1651+
ToolchainRequest::ImplementationVersion(
1652+
ImplementationName::PyPy,
1653+
VersionRequest::from_str("3.10").unwrap()
1654+
)
16071655
);
16081656

16091657
let tempdir = TempDir::new().unwrap();
@@ -1645,5 +1693,28 @@ mod tests {
16451693
VersionRequest::MajorMinorPatch(3, 12, 1)
16461694
);
16471695
assert!(VersionRequest::from_str("1.foo.1").is_err());
1696+
assert_eq!(
1697+
VersionRequest::from_str("3").unwrap(),
1698+
VersionRequest::Major(3)
1699+
);
1700+
assert_eq!(
1701+
VersionRequest::from_str("38").unwrap(),
1702+
VersionRequest::MajorMinor(3, 8)
1703+
);
1704+
assert_eq!(
1705+
VersionRequest::from_str("312").unwrap(),
1706+
VersionRequest::MajorMinor(3, 12)
1707+
);
1708+
assert_eq!(
1709+
VersionRequest::from_str("3100").unwrap(),
1710+
VersionRequest::MajorMinor(3, 100)
1711+
);
1712+
assert!(
1713+
// Test for overflow
1714+
matches!(
1715+
VersionRequest::from_str("31000"),
1716+
Err(Error::InvalidVersionRequest(_))
1717+
)
1718+
);
16481719
}
16491720
}

crates/uv-toolchain/src/implementation.rs

+7-6
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@ pub enum LenientImplementationName {
2424
}
2525

2626
impl ImplementationName {
27-
pub(crate) fn iter() -> impl Iterator<Item = &'static ImplementationName> {
28-
static NAMES: &[ImplementationName] =
29-
&[ImplementationName::CPython, ImplementationName::PyPy];
30-
NAMES.iter()
27+
pub(crate) fn possible_names() -> impl Iterator<Item = &'static str> {
28+
["cpython", "pypy", "cp", "pp"].into_iter()
3129
}
3230

3331
pub fn pretty(self) -> &'static str {
@@ -68,10 +66,13 @@ impl<'a> From<&'a LenientImplementationName> for &'a str {
6866
impl FromStr for ImplementationName {
6967
type Err = Error;
7068

69+
/// Parse a Python implementation name from a string.
70+
///
71+
/// Supports the full name and the platform compatibility tag style name.
7172
fn from_str(s: &str) -> Result<Self, Self::Err> {
7273
match s.to_ascii_lowercase().as_str() {
73-
"cpython" => Ok(Self::CPython),
74-
"pypy" => Ok(Self::PyPy),
74+
"cpython" | "cp" => Ok(Self::CPython),
75+
"pypy" | "pp" => Ok(Self::PyPy),
7576
_ => Err(Error::UnknownImplementation(s.to_string())),
7677
}
7778
}

0 commit comments

Comments
 (0)