Improve Python executable name discovery when using alternative implementations (#7649)

There are two parts to this. 

The first is a restructuring and refactoring. We had some debt around
expected executable name generation, which we address here by
consolidating into a single function that generates a combination of
names. This includes a bit of extra code around free-threaded variants
because this was written on top of #7431 — I'll rebase that on top of
this.

The second addresses some bugs around alternative implementations.
Notably, `uv python list` does not discovery executables with
alternative implementation names. Now, we properly generate all of the
executable names for `VersionRequest::Any` (originally implemented in
https://github.com/astral-sh/uv/pull/7508) to properly show all the
implementations we can find:

```
❯ cargo run -q -- python list --no-python-downloads
cpython-3.12.6-macos-aarch64-none     /opt/homebrew/opt/python@3.12/bin/python3.12 -> ../Frameworks/Python.framework/Versions/3.12/bin/python3.12
cpython-3.11.10-macos-aarch64-none    /opt/homebrew/opt/python@3.11/bin/python3.11 -> ../Frameworks/Python.framework/Versions/3.11/bin/python3.11
cpython-3.9.6-macos-aarch64-none      /Library/Developer/CommandLineTools/usr/bin/python3 -> ../../Library/Frameworks/Python3.framework/Versions/3.9/bin/python3
pypy-3.10.14-macos-aarch64-none       /opt/homebrew/bin/pypy3 -> ../Cellar/pypy3.10/7.3.17/bin/pypy3
```

While doing both of these changes, I ended up changing the priority of
interpreter discovery slightly. For example, given that the executables
are in the same directory, do we query `python` or `python3.10` first
when you request `--python 3.10`? Previously, we'd check `python3.10`
but I think that was an incorrect optimization. I think we should always
prefer the bare name (i.e. `python`) first. Similarly, this applies to
`python` and an executable for an alternative implementation like
`pypy`. If it's not compatible with the request, we'll skip it anyway.
We might have to query more interpreters with this approach but it seems
rare.


Closes https://github.com/astral-sh/uv/issues/7286 superseding
https://github.com/astral-sh/uv/pull/7508
This commit is contained in:
Zanie Blue 2024-09-23 17:17:55 -05:00 committed by GitHub
parent 63b60bc0c8
commit 0dea932d83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 372 additions and 218 deletions

View file

@ -1983,124 +1983,6 @@ mod tests {
Ok(())
}
#[test]
fn find_python_pypy_prefers_executable_with_implementation_name() -> Result<()> {
let mut context = TestContext::new()?;
// We should prefer `pypy` executables over `python` executables in the same directory
// even if they are both pypy
TestContext::create_mock_interpreter(
&context.tempdir.join("python"),
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::PyPy,
true,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("pypy"),
&PythonVersion::from_str("3.10.1").unwrap(),
ImplementationName::PyPy,
true,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
let python = context.run(|| {
find_python_installation(
&PythonRequest::parse("pypy@3.10"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert_eq!(
python.interpreter().python_full_version().to_string(),
"3.10.1",
);
// But `python` executables earlier in the search path will take precedence
context.reset_search_path();
context.add_python_interpreters(&[
(true, ImplementationName::PyPy, "python", "3.10.2"),
(true, ImplementationName::PyPy, "pypy", "3.10.3"),
])?;
let python = context.run(|| {
find_python_installation(
&PythonRequest::parse("pypy@3.10"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert_eq!(
python.interpreter().python_full_version().to_string(),
"3.10.2",
);
Ok(())
}
#[test]
fn find_python_pypy_prefers_executable_with_version() -> Result<()> {
let mut context = TestContext::new()?;
TestContext::create_mock_interpreter(
&context.tempdir.join("pypy3.10"),
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::PyPy,
true,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("pypy"),
&PythonVersion::from_str("3.10.1").unwrap(),
ImplementationName::PyPy,
true,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
let python = context.run(|| {
find_python_installation(
&PythonRequest::parse("pypy@3.10"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert_eq!(
python.interpreter().python_full_version().to_string(),
"3.10.0",
"We should prefer executables with the version number over those with implementation names"
);
let mut context = TestContext::new()?;
TestContext::create_mock_interpreter(
&context.tempdir.join("python3.10"),
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::PyPy,
true,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("pypy"),
&PythonVersion::from_str("3.10.1").unwrap(),
ImplementationName::PyPy,
true,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
let python = context.run(|| {
find_python_installation(
&PythonRequest::parse("pypy@3.10"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert_eq!(
python.interpreter().python_full_version().to_string(),
"3.10.1",
"We should prefer an implementation name executable over a generic name with a version"
);
Ok(())
}
#[test]
fn find_python_graalpy() -> Result<()> {
let mut context = TestContext::new()?;
@ -2203,11 +2085,11 @@ mod tests {
}
#[test]
fn find_python_graalpy_prefers_executable_with_implementation_name() -> Result<()> {
fn find_python_prefers_generic_executable_over_implementation_name() -> Result<()> {
let mut context = TestContext::new()?;
// We should prefer `graalpy` executables over `python` executables in the same directory
// even if they are both graalpy
// We prefer `python` executables over `graalpy` executables in the same directory
// if they are both GraalPy
TestContext::create_mock_interpreter(
&context.tempdir.join("python"),
&PythonVersion::from_str("3.10.0").unwrap(),
@ -2232,10 +2114,10 @@ mod tests {
})??;
assert_eq!(
python.interpreter().python_full_version().to_string(),
"3.10.1",
"3.10.0",
);
// But `python` executables earlier in the search path will take precedence
// And `python` executables earlier in the search path will take precedence
context.reset_search_path();
context.add_python_interpreters(&[
(true, ImplementationName::GraalPy, "python", "3.10.2"),
@ -2254,6 +2136,88 @@ mod tests {
"3.10.2",
);
// But `graalpy` executables earlier in the search path will take precedence
context.reset_search_path();
context.add_python_interpreters(&[
(true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
(true, ImplementationName::GraalPy, "python", "3.10.2"),
])?;
let python = context.run(|| {
find_python_installation(
&PythonRequest::parse("graalpy@3.10"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert_eq!(
python.interpreter().python_full_version().to_string(),
"3.10.3",
);
Ok(())
}
#[test]
fn find_python_prefers_generic_executable_over_one_with_version() -> Result<()> {
let mut context = TestContext::new()?;
TestContext::create_mock_interpreter(
&context.tempdir.join("pypy3.10"),
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::PyPy,
true,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("pypy"),
&PythonVersion::from_str("3.10.1").unwrap(),
ImplementationName::PyPy,
true,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
let python = context.run(|| {
find_python_installation(
&PythonRequest::parse("pypy@3.10"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert_eq!(
python.interpreter().python_full_version().to_string(),
"3.10.1",
"We should prefer the generic executable over one with the version number"
);
let mut context = TestContext::new()?;
TestContext::create_mock_interpreter(
&context.tempdir.join("python3.10"),
&PythonVersion::from_str("3.10.0").unwrap(),
ImplementationName::PyPy,
true,
)?;
TestContext::create_mock_interpreter(
&context.tempdir.join("pypy"),
&PythonVersion::from_str("3.10.1").unwrap(),
ImplementationName::PyPy,
true,
)?;
context.add_to_search_path(context.tempdir.to_path_buf());
let python = context.run(|| {
find_python_installation(
&PythonRequest::parse("pypy@3.10"),
EnvironmentPreference::Any,
PythonPreference::OnlySystem,
&context.cache,
)
})??;
assert_eq!(
python.interpreter().python_full_version().to_string(),
"3.10.0",
"We should prefer the generic name with a version over one the implementation name"
);
Ok(())
}
}