Skip to content

Commit 2966471

Browse files
authored
Prefer Python executable names that match the request over default names (#9066)
This restores behavior previously removed in #7649. I thought it'd be clearer (and simpler) to have a consistent Python executable name ordering. However, we've seen some cases where this can be surprising and, in combination with #8481, can result in incorrect behavior. For example, see #9046 where we prefer `python3` over `python3.12` in the same directory even though `python3.12` was requested. While `python3` and `python3.12` both point to valid Python 3.12 environments there, the expectation is that when `python3.12` is requested that the `python3.12` executable is preferred. This expectation may be less obvious if the user requests `[email protected]`, but uv does not distinguish between these request forms. Similarly, this may be surprising as by default uv prefers `python` over `python3` but when requesting `python3.12` the preference will be swapped.
1 parent 15ef807 commit 2966471

File tree

5 files changed

+284
-157
lines changed

5 files changed

+284
-157
lines changed

crates/uv-python/src/discovery.rs

+106-18
Original file line numberDiff line numberDiff line change
@@ -1600,20 +1600,102 @@ impl EnvironmentPreference {
16001600
}
16011601
}
16021602

1603-
#[derive(Debug, Clone, Copy)]
1603+
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
16041604
pub(crate) struct ExecutableName {
1605-
name: &'static str,
1605+
implementation: Option<ImplementationName>,
16061606
major: Option<u8>,
16071607
minor: Option<u8>,
16081608
patch: Option<u8>,
16091609
prerelease: Option<Prerelease>,
16101610
variant: PythonVariant,
16111611
}
16121612

1613+
#[derive(Debug, Clone, PartialEq, Eq)]
1614+
struct ExecutableNameComparator<'a> {
1615+
name: ExecutableName,
1616+
request: &'a VersionRequest,
1617+
implementation: Option<&'a ImplementationName>,
1618+
}
1619+
1620+
impl Ord for ExecutableNameComparator<'_> {
1621+
/// Note the comparison returns a reverse priority ordering.
1622+
///
1623+
/// Higher priority items are "Greater" than lower priority items.
1624+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1625+
// Prefer the default name over a specific implementation, unless an implementation was
1626+
// requested
1627+
let name_ordering = if self.implementation.is_some() {
1628+
std::cmp::Ordering::Greater
1629+
} else {
1630+
std::cmp::Ordering::Less
1631+
};
1632+
if self.name.implementation.is_none() && other.name.implementation.is_some() {
1633+
return name_ordering.reverse();
1634+
}
1635+
if self.name.implementation.is_some() && other.name.implementation.is_none() {
1636+
return name_ordering;
1637+
}
1638+
// Otherwise, use the names in supported order
1639+
let ordering = self.name.implementation.cmp(&other.name.implementation);
1640+
if ordering != std::cmp::Ordering::Equal {
1641+
return ordering;
1642+
}
1643+
let ordering = self.name.major.cmp(&other.name.major);
1644+
let is_default_request =
1645+
matches!(self.request, VersionRequest::Any | VersionRequest::Default);
1646+
if ordering != std::cmp::Ordering::Equal {
1647+
return if is_default_request {
1648+
ordering.reverse()
1649+
} else {
1650+
ordering
1651+
};
1652+
}
1653+
let ordering = self.name.minor.cmp(&other.name.minor);
1654+
if ordering != std::cmp::Ordering::Equal {
1655+
return if is_default_request {
1656+
ordering.reverse()
1657+
} else {
1658+
ordering
1659+
};
1660+
}
1661+
let ordering = self.name.patch.cmp(&other.name.patch);
1662+
if ordering != std::cmp::Ordering::Equal {
1663+
return if is_default_request {
1664+
ordering.reverse()
1665+
} else {
1666+
ordering
1667+
};
1668+
}
1669+
let ordering = self.name.prerelease.cmp(&other.name.prerelease);
1670+
if ordering != std::cmp::Ordering::Equal {
1671+
return if is_default_request {
1672+
ordering.reverse()
1673+
} else {
1674+
ordering
1675+
};
1676+
}
1677+
let ordering = self.name.variant.cmp(&other.name.variant);
1678+
if ordering != std::cmp::Ordering::Equal {
1679+
return if is_default_request {
1680+
ordering.reverse()
1681+
} else {
1682+
ordering
1683+
};
1684+
}
1685+
ordering
1686+
}
1687+
}
1688+
1689+
impl PartialOrd for ExecutableNameComparator<'_> {
1690+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1691+
Some(self.cmp(other))
1692+
}
1693+
}
1694+
16131695
impl ExecutableName {
16141696
#[must_use]
1615-
fn with_name(mut self, name: &'static str) -> Self {
1616-
self.name = name;
1697+
fn with_implementation(mut self, implementation: ImplementationName) -> Self {
1698+
self.implementation = Some(implementation);
16171699
self
16181700
}
16191701

@@ -1646,24 +1728,27 @@ impl ExecutableName {
16461728
self.variant = variant;
16471729
self
16481730
}
1649-
}
16501731

1651-
impl Default for ExecutableName {
1652-
fn default() -> Self {
1653-
Self {
1654-
name: "python",
1655-
major: None,
1656-
minor: None,
1657-
patch: None,
1658-
prerelease: None,
1659-
variant: PythonVariant::Default,
1732+
fn into_comparator<'a>(
1733+
self,
1734+
request: &'a VersionRequest,
1735+
implementation: Option<&'a ImplementationName>,
1736+
) -> ExecutableNameComparator<'a> {
1737+
ExecutableNameComparator {
1738+
name: self,
1739+
request,
1740+
implementation,
16601741
}
16611742
}
16621743
}
16631744

16641745
impl std::fmt::Display for ExecutableName {
16651746
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1666-
write!(f, "{}", self.name)?;
1747+
if let Some(implementation) = self.implementation {
1748+
write!(f, "{implementation}")?;
1749+
} else {
1750+
f.write_str("python")?;
1751+
}
16671752
if let Some(major) = self.major {
16681753
write!(f, "{major}")?;
16691754
if let Some(minor) = self.minor {
@@ -1741,15 +1826,15 @@ impl VersionRequest {
17411826
// Add all the implementation-specific names
17421827
if let Some(implementation) = implementation {
17431828
for i in 0..names.len() {
1744-
let name = names[i].with_name(implementation.into());
1829+
let name = names[i].with_implementation(*implementation);
17451830
names.push(name);
17461831
}
17471832
} else {
17481833
// When looking for all implementations, include all possible names
17491834
if matches!(self, Self::Any) {
17501835
for i in 0..names.len() {
1751-
for implementation in ImplementationName::long_names() {
1752-
let name = names[i].with_name(implementation);
1836+
for implementation in ImplementationName::iter_all() {
1837+
let name = names[i].with_implementation(implementation);
17531838
names.push(name);
17541839
}
17551840
}
@@ -1764,6 +1849,9 @@ impl VersionRequest {
17641849
}
17651850
}
17661851

1852+
names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
1853+
names.reverse();
1854+
17671855
names
17681856
}
17691857

crates/uv-python/src/discovery/tests.rs

+18-15
Original file line numberDiff line numberDiff line change
@@ -477,49 +477,52 @@ fn executable_names_from_request() {
477477
case(
478478
"any",
479479
&[
480-
"python", "python3", "cpython", "pypy", "graalpy", "cpython3", "pypy3", "graalpy3",
480+
"python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
481481
],
482482
);
483483

484484
case("default", &["python", "python3"]);
485485

486-
case("3", &["python", "python3"]);
486+
case("3", &["python3", "python"]);
487487

488-
case("4", &["python", "python4"]);
488+
case("4", &["python4", "python"]);
489489

490-
case("3.13", &["python", "python3", "python3.13"]);
490+
case("3.13", &["python3.13", "python3", "python"]);
491+
492+
case("pypy", &["pypy", "pypy3", "python", "python3"]);
491493

492494
case(
493495
494496
&[
495-
"python",
496-
"python3",
497-
"python3.10",
498-
"pypy",
499-
"pypy3",
500497
"pypy3.10",
498+
"pypy3",
499+
"pypy",
500+
"python3.10",
501+
"python3",
502+
"python",
501503
],
502504
);
503505

504506
case(
505507
"3.13t",
506508
&[
507-
"python",
508-
"python3",
509+
"python3.13t",
509510
"python3.13",
510-
"pythont",
511511
"python3t",
512-
"python3.13t",
512+
"python3",
513+
"pythont",
514+
"python",
513515
],
514516
);
517+
case("3t", &["python3t", "python3", "pythont", "python"]);
515518

516519
case(
517520
"3.13.2",
518-
&["python", "python3", "python3.13", "python3.13.2"],
521+
&["python3.13.2", "python3.13", "python3", "python"],
519522
);
520523

521524
case(
522525
"3.13rc2",
523-
&["python", "python3", "python3.13", "python3.13rc2"],
526+
&["python3.13rc2", "python3.13", "python3", "python"],
524527
);
525528
}

crates/uv-python/src/implementation.rs

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ impl ImplementationName {
3333
["cpython", "pypy", "graalpy"].into_iter()
3434
}
3535

36+
pub(crate) fn iter_all() -> impl Iterator<Item = Self> {
37+
[Self::CPython, Self::PyPy, Self::GraalPy].into_iter()
38+
}
39+
3640
pub fn pretty(self) -> &'static str {
3741
match self {
3842
Self::CPython => "CPython",

0 commit comments

Comments
 (0)