mirror of https://github.com/astral-sh/uv
Rewrite Python interpreter discovery (#3266)
Updates our Python interpreter discovery to conform to the rules described in #2386, please see that issue for a full description of the behavior. Briefly, we now will search for interpreters that satisfy a requested version without stopping at the first Python executable. Additionally, if retrieving information about an interpreter fails we will continue to search for a working interpreter. We also add the plumbing necessary to request Python implementations other than CPython, though we do not add support for other implementations at this time. A major internal goal of this work is to prepare for user-facing managed toolchains i.e. fetching a requested version during `uv run`. These APIs are not introduced, but there is some managed toolchain handling as required for our test suite. Some noteworthy implementation changes: - The `uv_interpreter::find_python` module has been removed in favor of a `uv_interpreter::discovery` module. - There are new types to help structure interpreter requests and track sources - Executable discovery is implemented as a big lazy iterator and is a central authority for source precedence - `uv_interpreter::Error` variants were split into scoped types in each module - There's much more unit test coverage, but not for Windows yet Remaining work: - [x] Write new test cases - [x] Determine correct behavior around executables in the current directory - _Future_: Combine `PythonVersion` and `VersionRequest` - _Future_: Consider splitting `ManagedToolchain` into local and remote variants - _Future_: Add Windows unit test coverage - _Future_: Explore behavior around implementation precedence (i.e. CPython over PyPy) Refactors split into: - #3329 - #3330 - #3331 - #3332 Closes #2386
This commit is contained in:
parent
c14a7dbef3
commit
d540d0f28b
|
|
@ -144,6 +144,7 @@ jobs:
|
|||
|
||||
- name: "Cargo test"
|
||||
run: |
|
||||
export UV_BOOTSTRAP_DIR="$(pwd)/bin"
|
||||
cargo nextest run \
|
||||
--workspace \
|
||||
--status-level skip --failure-output immediate-final --no-fail-fast -j 12 --final-status-level slow
|
||||
|
|
|
|||
|
|
@ -3821,6 +3821,15 @@ version = "0.1.13"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f227968ec00f0e5322f9b8173c7a0cbcff6181a0a5b28e9892491c286277231"
|
||||
|
||||
[[package]]
|
||||
name = "temp-env"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050"
|
||||
dependencies = [
|
||||
"parking_lot 0.12.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.1"
|
||||
|
|
@ -4889,12 +4898,12 @@ name = "uv-interpreter"
|
|||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_fs",
|
||||
"cache-key",
|
||||
"configparser",
|
||||
"fs-err",
|
||||
"futures",
|
||||
"indoc",
|
||||
"insta",
|
||||
"install-wheel-rs",
|
||||
"itertools 0.13.0",
|
||||
"once_cell",
|
||||
|
|
@ -4910,7 +4919,9 @@ dependencies = [
|
|||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"temp-env",
|
||||
"tempfile",
|
||||
"test-log",
|
||||
"thiserror",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
|
|
|
|||
|
|
@ -25,11 +25,13 @@ pub(crate) struct FetchPythonArgs {
|
|||
pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
|
||||
let bootstrap_dir = TOOLCHAIN_DIRECTORY
|
||||
.as_ref()
|
||||
.expect("The toolchain directory must exist for bootstrapping");
|
||||
let bootstrap_dir = TOOLCHAIN_DIRECTORY.clone().unwrap_or_else(|| {
|
||||
std::env::current_dir()
|
||||
.expect("Use `UV_BOOTSTRAP_DIR` if the current directory is not usable.")
|
||||
.join("bin")
|
||||
});
|
||||
|
||||
fs_err::create_dir_all(bootstrap_dir)?;
|
||||
fs_err::create_dir_all(&bootstrap_dir)?;
|
||||
|
||||
let versions = if args.versions.is_empty() {
|
||||
info!("Reading versions from file...");
|
||||
|
|
@ -59,7 +61,7 @@ pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
|
|||
let mut tasks = futures::stream::iter(downloads.iter())
|
||||
.map(|download| {
|
||||
async {
|
||||
let result = download.fetch(&client, bootstrap_dir).await;
|
||||
let result = download.fetch(&client, &bootstrap_dir).await;
|
||||
(download.python_version(), result)
|
||||
}
|
||||
.instrument(info_span!("download", key = %download))
|
||||
|
|
@ -130,6 +132,10 @@ pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> {
|
|||
};
|
||||
|
||||
info!("Installed {} versions", requests.len());
|
||||
info!(
|
||||
r#"To enable discovery: export UV_BOOTSTRAP_DIR="{}""#,
|
||||
bootstrap_dir.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,9 @@ winapi = { workspace = true }
|
|||
|
||||
[dev-dependencies]
|
||||
anyhow = { version = "1.0.80" }
|
||||
assert_fs = { version = "1.1.1" }
|
||||
indoc = { version = "2.0.4" }
|
||||
insta = { version = "1.36.1", features = ["filters"] }
|
||||
itertools = { version = "0.13.0" }
|
||||
temp-env = { version = "0.3.6" }
|
||||
tempfile = { version = "3.9.0" }
|
||||
test-log = { version = "0.2.15", features = ["trace"], default-features = false }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,8 +8,9 @@ use same_file::is_same_file;
|
|||
use uv_cache::Cache;
|
||||
use uv_fs::{LockedFile, Simplified};
|
||||
|
||||
use crate::virtualenv::{detect_virtualenv, virtualenv_python_executable, PyVenvConfiguration};
|
||||
use crate::{find_default_python, find_requested_python, Error, Interpreter, Target};
|
||||
use crate::discovery::{InterpreterRequest, SourceSelector, SystemPython, VersionRequest};
|
||||
use crate::virtualenv::{virtualenv_python_executable, PyVenvConfiguration};
|
||||
use crate::{find_default_interpreter, find_interpreter, Error, Interpreter, Target};
|
||||
|
||||
/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -22,13 +23,39 @@ struct PythonEnvironmentShared {
|
|||
}
|
||||
|
||||
impl PythonEnvironment {
|
||||
/// Create a [`PythonEnvironment`] for an existing virtual environment, detected from the
|
||||
/// environment variables and filesystem.
|
||||
/// Create a [`PythonEnvironment`] from a user request.
|
||||
pub fn find(python: Option<&str>, system: SystemPython, cache: &Cache) -> Result<Self, Error> {
|
||||
// Detect the current Python interpreter.
|
||||
if let Some(python) = python {
|
||||
Self::from_requested_python(python, system, cache)
|
||||
} else if system.is_preferred() {
|
||||
Self::from_default_python(cache)
|
||||
} else {
|
||||
match Self::from_virtualenv(cache) {
|
||||
Ok(venv) => Ok(venv),
|
||||
Err(Error::NotFound(_)) if system.is_allowed() => Self::from_default_python(cache),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`PythonEnvironment`] for an existing virtual environment.
|
||||
pub fn from_virtualenv(cache: &Cache) -> Result<Self, Error> {
|
||||
let Some(venv) = detect_virtualenv()? else {
|
||||
return Err(Error::VenvNotFound);
|
||||
};
|
||||
Self::from_root(&venv, cache)
|
||||
let sources = SourceSelector::virtualenvs();
|
||||
let request = InterpreterRequest::Version(VersionRequest::Default);
|
||||
let found = find_interpreter(&request, &sources, cache)??;
|
||||
|
||||
debug_assert!(
|
||||
found.interpreter().base_prefix() == found.interpreter().base_exec_prefix(),
|
||||
"Not a virtualenv (source: {}, prefix: {})",
|
||||
found.source(),
|
||||
found.interpreter().base_prefix().display()
|
||||
);
|
||||
|
||||
Ok(Self(Arc::new(PythonEnvironmentShared {
|
||||
root: found.interpreter().prefix().to_path_buf(),
|
||||
interpreter: found.into_interpreter(),
|
||||
})))
|
||||
}
|
||||
|
||||
/// Create a [`PythonEnvironment`] from the virtual environment at the given root.
|
||||
|
|
@ -36,31 +63,30 @@ impl PythonEnvironment {
|
|||
let venv = match fs_err::canonicalize(root) {
|
||||
Ok(venv) => venv,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Err(Error::VenvDoesNotExist(root.to_path_buf()));
|
||||
return Err(Error::NotFound(
|
||||
crate::InterpreterNotFound::DirectoryNotFound(root.to_path_buf()),
|
||||
));
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
Err(err) => return Err(Error::Discovery(err.into())),
|
||||
};
|
||||
let executable = virtualenv_python_executable(&venv);
|
||||
let interpreter = Interpreter::query(&executable, cache)?;
|
||||
|
||||
debug_assert!(
|
||||
interpreter.base_prefix() == interpreter.base_exec_prefix(),
|
||||
"Not a virtualenv (Python: {}, prefix: {})",
|
||||
executable.display(),
|
||||
interpreter.base_prefix().display()
|
||||
);
|
||||
let executable = virtualenv_python_executable(venv);
|
||||
let interpreter = Interpreter::query(executable, cache)?;
|
||||
|
||||
Ok(Self(Arc::new(PythonEnvironmentShared {
|
||||
root: venv,
|
||||
root: interpreter.prefix().to_path_buf(),
|
||||
interpreter,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Create a [`PythonEnvironment`] for a Python interpreter specifier (e.g., a path or a binary name).
|
||||
pub fn from_requested_python(python: &str, cache: &Cache) -> Result<Self, Error> {
|
||||
let Some(interpreter) = find_requested_python(python, cache)? else {
|
||||
return Err(Error::RequestedPythonNotFound(python.to_string()));
|
||||
};
|
||||
pub fn from_requested_python(
|
||||
request: &str,
|
||||
system: SystemPython,
|
||||
cache: &Cache,
|
||||
) -> Result<Self, Error> {
|
||||
let sources = SourceSelector::from_env(system);
|
||||
let request = InterpreterRequest::parse(request);
|
||||
let interpreter = find_interpreter(&request, &sources, cache)??.into_interpreter();
|
||||
Ok(Self(Arc::new(PythonEnvironmentShared {
|
||||
root: interpreter.prefix().to_path_buf(),
|
||||
interpreter,
|
||||
|
|
@ -69,14 +95,14 @@ impl PythonEnvironment {
|
|||
|
||||
/// Create a [`PythonEnvironment`] for the default Python interpreter.
|
||||
pub fn from_default_python(cache: &Cache) -> Result<Self, Error> {
|
||||
let interpreter = find_default_python(cache)?;
|
||||
let interpreter = find_default_interpreter(cache)??.into_interpreter();
|
||||
Ok(Self(Arc::new(PythonEnvironmentShared {
|
||||
root: interpreter.prefix().to_path_buf(),
|
||||
interpreter,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and root directory.
|
||||
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`].
|
||||
pub fn from_interpreter(interpreter: Interpreter) -> Self {
|
||||
Self(Arc::new(PythonEnvironmentShared {
|
||||
root: interpreter.prefix().to_path_buf(),
|
||||
|
|
@ -100,11 +126,13 @@ impl PythonEnvironment {
|
|||
}
|
||||
|
||||
/// Return the [`Interpreter`] for this virtual environment.
|
||||
///
|
||||
/// See also [`PythonEnvironment::into_interpreter`].
|
||||
pub fn interpreter(&self) -> &Interpreter {
|
||||
&self.0.interpreter
|
||||
}
|
||||
|
||||
/// Return the [`PyVenvConfiguration`] for this virtual environment, as extracted from the
|
||||
/// Return the [`PyVenvConfiguration`] for this environment, as extracted from the
|
||||
/// `pyvenv.cfg` file.
|
||||
pub fn cfg(&self) -> Result<PyVenvConfiguration, Error> {
|
||||
Ok(PyVenvConfiguration::parse(self.0.root.join("pyvenv.cfg"))?)
|
||||
|
|
@ -115,7 +143,7 @@ impl PythonEnvironment {
|
|||
self.0.interpreter.sys_executable()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the `site-packages` directories inside a virtual environment.
|
||||
/// Returns an iterator over the `site-packages` directories inside the environment.
|
||||
///
|
||||
/// In most cases, `purelib` and `platlib` will be the same, and so the iterator will contain
|
||||
/// a single element; however, in some distributions, they may be different.
|
||||
|
|
@ -138,12 +166,12 @@ impl PythonEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the path to the `bin` directory inside a virtual environment.
|
||||
/// Returns the path to the `bin` directory inside this environment.
|
||||
pub fn scripts(&self) -> &Path {
|
||||
self.0.interpreter.scripts()
|
||||
}
|
||||
|
||||
/// Grab a file lock for the virtual environment to prevent concurrent writes across processes.
|
||||
/// Grab a file lock for the environment to prevent concurrent writes across processes.
|
||||
pub fn lock(&self) -> Result<LockedFile, std::io::Error> {
|
||||
if let Some(target) = self.0.interpreter.target() {
|
||||
// If we're installing into a `--target`, use a target-specific lock file.
|
||||
|
|
@ -163,7 +191,9 @@ impl PythonEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the [`Interpreter`] for this virtual environment.
|
||||
/// Return the [`Interpreter`] for this environment.
|
||||
///
|
||||
/// See also [`PythonEnvironment::interpreter`].
|
||||
pub fn into_interpreter(self) -> Interpreter {
|
||||
Arc::unwrap_or_clone(self.0).interpreter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,800 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
use std::env;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::interpreter::InterpreterInfoError;
|
||||
use crate::py_launcher::{py_list_paths, Error as PyLauncherError, PyListPath};
|
||||
use crate::virtualenv::{detect_virtualenv, virtualenv_python_executable};
|
||||
use crate::PythonVersion;
|
||||
use crate::{Error, Interpreter};
|
||||
|
||||
/// Find a Python of a specific version, a binary with a name or a path to a binary.
|
||||
///
|
||||
/// Supported formats:
|
||||
/// * `-p 3.10` searches for an installed Python 3.10 (`py --list-paths` on Windows, `python3.10` on
|
||||
/// Linux/Mac). Specifying a patch version is not supported.
|
||||
/// * `-p python3.10` or `-p python.exe` looks for a binary in `PATH`.
|
||||
/// * `-p /home/ferris/.local/bin/python3.10` uses this exact Python.
|
||||
///
|
||||
/// When the user passes a patch version (e.g. 3.12.1), we currently search for a matching minor
|
||||
/// version (e.g. `python3.12` on unix) and error when the version mismatches, as a binary with the
|
||||
/// patch version (e.g. `python3.12.1`) is often not in `PATH` and we make the simplifying
|
||||
/// assumption that the user has only this one patch version installed.
|
||||
#[instrument(skip_all, fields(%request))]
|
||||
pub fn find_requested_python(request: &str, cache: &Cache) -> Result<Option<Interpreter>, Error> {
|
||||
debug!("Starting interpreter discovery for Python @ `{request}`");
|
||||
let versions = request
|
||||
.splitn(3, '.')
|
||||
.map(str::parse::<u8>)
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
if let Ok(versions) = versions {
|
||||
// `-p 3.10` or `-p 3.10.1`
|
||||
let selector = match versions.as_slice() {
|
||||
[requested_major] => PythonVersionSelector::Major(*requested_major),
|
||||
[major, minor] => PythonVersionSelector::MajorMinor(*major, *minor),
|
||||
[major, minor, requested_patch] => {
|
||||
PythonVersionSelector::MajorMinorPatch(*major, *minor, *requested_patch)
|
||||
}
|
||||
// SAFETY: Guaranteed by the Ok(versions) guard
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let interpreter = find_python(selector, cache)?;
|
||||
interpreter
|
||||
.as_ref()
|
||||
.inspect(|inner| warn_on_unsupported_python(inner));
|
||||
Ok(interpreter)
|
||||
} else {
|
||||
match fs_err::metadata(request) {
|
||||
Ok(metadata) => {
|
||||
// Map from user-provided path to an executable.
|
||||
let path = uv_fs::absolutize_path(request.as_ref())?;
|
||||
let executable = if metadata.is_dir() {
|
||||
// If the user provided a directory, assume it's a virtual environment.
|
||||
// `-p /home/ferris/.venv`
|
||||
if cfg!(windows) {
|
||||
Cow::Owned(path.join("Scripts/python.exe"))
|
||||
} else {
|
||||
Cow::Owned(path.join("bin/python"))
|
||||
}
|
||||
} else {
|
||||
// Otherwise, assume it's a Python executable.
|
||||
// `-p /home/ferris/.local/bin/python3.10`
|
||||
path
|
||||
};
|
||||
Interpreter::query(executable, cache)
|
||||
.inspect(warn_on_unsupported_python)
|
||||
.map(Some)
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
// `-p python3.10`; Generally not used on windows because all Python are `python.exe`.
|
||||
let Some(executable) = find_executable(request)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Interpreter::query(executable, cache)
|
||||
.inspect(warn_on_unsupported_python)
|
||||
.map(Some)
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick a sensible default for the Python a user wants when they didn't specify a version.
|
||||
///
|
||||
/// We prefer the test overwrite `UV_TEST_PYTHON_PATH` if it is set, otherwise `python3`/`python` or
|
||||
/// `python.exe` respectively.
|
||||
#[instrument(skip_all)]
|
||||
pub fn find_default_python(cache: &Cache) -> Result<Interpreter, Error> {
|
||||
debug!("Starting interpreter discovery for default Python");
|
||||
try_find_default_python(cache)?
|
||||
.ok_or(if cfg!(windows) {
|
||||
Error::NoPythonInstalledWindows
|
||||
} else if cfg!(unix) {
|
||||
Error::NoPythonInstalledUnix
|
||||
} else {
|
||||
unreachable!("Only Unix and Windows are supported")
|
||||
})
|
||||
.inspect(warn_on_unsupported_python)
|
||||
}
|
||||
|
||||
/// Same as [`find_default_python`] but returns `None` if no python is found instead of returning an `Err`.
|
||||
pub(crate) fn try_find_default_python(cache: &Cache) -> Result<Option<Interpreter>, Error> {
|
||||
find_python(PythonVersionSelector::Default, cache)
|
||||
}
|
||||
|
||||
/// Find a Python version matching `selector`.
|
||||
///
|
||||
/// It searches for an existing installation in the following order:
|
||||
/// * Search for the python binary in `PATH` (or `UV_TEST_PYTHON_PATH` if set). Visits each path and for each path resolves the
|
||||
/// files in the following order:
|
||||
/// * Major.Minor.Patch: `pythonx.y.z`, `pythonx.y`, `python.x`, `python`
|
||||
/// * Major.Minor: `pythonx.y`, `pythonx`, `python`
|
||||
/// * Major: `pythonx`, `python`
|
||||
/// * Default: `python3`, `python`
|
||||
/// * (windows): For each of the above, test for the existence of `python.bat` shim (pyenv-windows) last.
|
||||
/// * (windows): Discover installations using `py --list-paths` (PEP514). Continue if `py` is not installed.
|
||||
///
|
||||
/// (Windows): Filter out the Windows store shim (Enabled in Settings/Apps/Advanced app settings/App execution aliases).
|
||||
fn find_python(
|
||||
selector: PythonVersionSelector,
|
||||
cache: &Cache,
|
||||
) -> Result<Option<Interpreter>, Error> {
|
||||
#[allow(non_snake_case)]
|
||||
let UV_TEST_PYTHON_PATH = env::var_os("UV_TEST_PYTHON_PATH");
|
||||
|
||||
let use_override = UV_TEST_PYTHON_PATH.is_some();
|
||||
let possible_names = selector.possible_names();
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let PATH = UV_TEST_PYTHON_PATH
|
||||
.or(env::var_os("PATH"))
|
||||
.unwrap_or_default();
|
||||
|
||||
// We use `which` here instead of joining the paths ourselves because `which` checks for us if the python
|
||||
// binary is executable and exists. It also has some extra logic that handles inconsistent casing on Windows
|
||||
// and expands `~`.
|
||||
for path in env::split_paths(&PATH) {
|
||||
for name in possible_names.iter().flatten() {
|
||||
if let Ok(paths) = which::which_in_global(&**name, Some(&path)) {
|
||||
for path in paths {
|
||||
#[cfg(windows)]
|
||||
if windows::is_windows_store_shim(&path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let interpreter = match Interpreter::query(&path, cache) {
|
||||
Ok(interpreter) => interpreter,
|
||||
|
||||
// If the Python version is < 3.4, the `-I` flag is not supported, so
|
||||
// we can't run the script at all, and need to sniff it from the output.
|
||||
Err(Error::PythonSubcommandOutput { stderr, .. })
|
||||
if stderr.contains("Unknown option: -I") =>
|
||||
{
|
||||
// If the user _requested_ a version prior to 3.4, raise an error, as
|
||||
// 3.4 is the minimum supported version for invoking the interpreter
|
||||
// query script at all.
|
||||
match selector {
|
||||
PythonVersionSelector::Major(major) if major < 3 => {
|
||||
return Err(Error::UnsupportedPython(major.to_string()));
|
||||
}
|
||||
PythonVersionSelector::MajorMinor(major, minor)
|
||||
if (major, minor) < (3, 4) =>
|
||||
{
|
||||
return Err(Error::UnsupportedPython(format!(
|
||||
"{major}.{minor}"
|
||||
)));
|
||||
}
|
||||
PythonVersionSelector::MajorMinorPatch(major, minor, patch)
|
||||
if (major, minor) < (3, 4) =>
|
||||
{
|
||||
return Err(Error::UnsupportedPython(format!(
|
||||
"{major}.{minor}.{patch}"
|
||||
)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Found a Python installation that isn't supported by uv, skipping."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
Err(Error::QueryScript {
|
||||
err: InterpreterInfoError::UnsupportedPythonVersion { .. },
|
||||
..
|
||||
}) => {
|
||||
// If the user _requested_ a version prior to 3.7, raise an error, as
|
||||
// 3.7 is the minimum supported version for running the interpreter
|
||||
// query script.
|
||||
match selector {
|
||||
PythonVersionSelector::Major(major) if major < 3 => {
|
||||
return Err(Error::UnsupportedPython(major.to_string()));
|
||||
}
|
||||
PythonVersionSelector::MajorMinor(major, minor)
|
||||
if (major, minor) < (3, 7) =>
|
||||
{
|
||||
return Err(Error::UnsupportedPython(format!(
|
||||
"{major}.{minor}"
|
||||
)));
|
||||
}
|
||||
PythonVersionSelector::MajorMinorPatch(major, minor, patch)
|
||||
if (major, minor) < (3, 7) =>
|
||||
{
|
||||
return Err(Error::UnsupportedPython(format!(
|
||||
"{major}.{minor}.{patch}"
|
||||
)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Found a Python installation that isn't supported by uv, skipping."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
|
||||
let installation = PythonInstallation::Interpreter(interpreter);
|
||||
|
||||
if let Some(interpreter) = installation.select(selector, cache)? {
|
||||
return Ok(Some(interpreter));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Python's `venv` model doesn't have this case because they use the `sys.executable` by default
|
||||
// which is sufficient to support pyenv-windows. Unfortunately, we can't rely on the executing Python version.
|
||||
// That's why we explicitly search for a Python shim as last resort.
|
||||
if cfg!(windows) {
|
||||
if let Ok(shims) = which::which_in_global("python.bat", Some(&path)) {
|
||||
for shim in shims {
|
||||
let interpreter = match Interpreter::query(&shim, cache) {
|
||||
Ok(interpreter) => interpreter,
|
||||
Err(error) => {
|
||||
// Don't fail when querying the shim failed. E.g it's possible that no python version is selected
|
||||
// in the shim in which case pyenv prints to stdout.
|
||||
tracing::warn!("Failed to query python shim: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(interpreter) =
|
||||
PythonInstallation::Interpreter(interpreter).select(selector, cache)?
|
||||
{
|
||||
return Ok(Some(interpreter));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(windows) && !use_override {
|
||||
// Use `py` to find the python installation on the system.
|
||||
match py_list_paths() {
|
||||
Ok(paths) => {
|
||||
for entry in paths {
|
||||
let installation = PythonInstallation::PyListPath(entry);
|
||||
if let Some(interpreter) = installation.select(selector, cache)? {
|
||||
return Ok(Some(interpreter));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not error when `py` is not available
|
||||
Err(PyLauncherError::NotFound) => debug!("`py` is not installed"),
|
||||
Err(error) => return Err(Error::PyLauncher(error)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Find the Python interpreter in `PATH` matching the given name (e.g., `python3`), respecting
|
||||
/// `UV_PYTHON_PATH`.
|
||||
///
|
||||
/// Returns `Ok(None)` if not found.
|
||||
fn find_executable<R: AsRef<OsStr> + Into<OsString> + Copy>(
|
||||
requested: R,
|
||||
) -> Result<Option<PathBuf>, Error> {
|
||||
#[allow(non_snake_case)]
|
||||
let UV_TEST_PYTHON_PATH = env::var_os("UV_TEST_PYTHON_PATH");
|
||||
|
||||
let use_override = UV_TEST_PYTHON_PATH.is_some();
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let PATH = UV_TEST_PYTHON_PATH
|
||||
.or(env::var_os("PATH"))
|
||||
.unwrap_or_default();
|
||||
|
||||
// We use `which` here instead of joining the paths ourselves because `which` checks for us if the python
|
||||
// binary is executable and exists. It also has some extra logic that handles inconsistent casing on Windows
|
||||
// and expands `~`.
|
||||
for path in env::split_paths(&PATH) {
|
||||
let paths = match which::which_in_global(requested, Some(&path)) {
|
||||
Ok(paths) => paths,
|
||||
Err(which::Error::CannotFindBinaryPath) => continue,
|
||||
Err(err) => return Err(Error::WhichError(requested.into(), err)),
|
||||
};
|
||||
|
||||
#[allow(clippy::never_loop)]
|
||||
for path in paths {
|
||||
#[cfg(windows)]
|
||||
if windows::is_windows_store_shim(&path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(windows) && !use_override {
|
||||
// Use `py` to find the python installation on the system.
|
||||
match py_list_paths() {
|
||||
Ok(paths) => {
|
||||
for entry in paths {
|
||||
// Ex) `--python python3.12.exe`
|
||||
if entry.executable_path.file_name() == Some(requested.as_ref()) {
|
||||
return Ok(Some(entry.executable_path));
|
||||
}
|
||||
|
||||
// Ex) `--python python3.12`
|
||||
if entry
|
||||
.executable_path
|
||||
.file_stem()
|
||||
.is_some_and(|stem| stem == requested.as_ref())
|
||||
{
|
||||
return Ok(Some(entry.executable_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not error when `py` is not available
|
||||
Err(PyLauncherError::NotFound) => debug!("`py` is not installed"),
|
||||
Err(error) => return Err(Error::PyLauncher(error)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum PythonInstallation {
|
||||
PyListPath(PyListPath),
|
||||
Interpreter(Interpreter),
|
||||
}
|
||||
|
||||
impl PythonInstallation {
|
||||
fn major(&self) -> u8 {
|
||||
match self {
|
||||
Self::PyListPath(PyListPath { major, .. }) => *major,
|
||||
Self::Interpreter(interpreter) => interpreter.python_major(),
|
||||
}
|
||||
}
|
||||
|
||||
fn minor(&self) -> u8 {
|
||||
match self {
|
||||
Self::PyListPath(PyListPath { minor, .. }) => *minor,
|
||||
Self::Interpreter(interpreter) => interpreter.python_minor(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects the interpreter if it matches the selector (version specification).
|
||||
fn select(
|
||||
self,
|
||||
selector: PythonVersionSelector,
|
||||
cache: &Cache,
|
||||
) -> Result<Option<Interpreter>, Error> {
|
||||
let selected = match selector {
|
||||
PythonVersionSelector::Default => true,
|
||||
|
||||
PythonVersionSelector::Major(major) => self.major() == major,
|
||||
|
||||
PythonVersionSelector::MajorMinor(major, minor) => {
|
||||
self.major() == major && self.minor() == minor
|
||||
}
|
||||
|
||||
PythonVersionSelector::MajorMinorPatch(major, minor, requested_patch) => {
|
||||
let interpreter = self.into_interpreter(cache)?;
|
||||
return Ok(
|
||||
if major == interpreter.python_major()
|
||||
&& minor == interpreter.python_minor()
|
||||
&& requested_patch == interpreter.python_patch()
|
||||
{
|
||||
Some(interpreter)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if selected {
|
||||
self.into_interpreter(cache).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn into_interpreter(self, cache: &Cache) -> Result<Interpreter, Error> {
|
||||
match self {
|
||||
Self::PyListPath(PyListPath {
|
||||
executable_path, ..
|
||||
}) => Interpreter::query(executable_path, cache),
|
||||
Self::Interpreter(interpreter) => Ok(interpreter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PythonVersionSelector {
|
||||
Default,
|
||||
Major(u8),
|
||||
MajorMinor(u8, u8),
|
||||
MajorMinorPatch(u8, u8, u8),
|
||||
}
|
||||
|
||||
impl PythonVersionSelector {
|
||||
fn possible_names(self) -> [Option<Cow<'static, str>>; 4] {
|
||||
let (python, python3, extension) = if cfg!(windows) {
|
||||
(
|
||||
Cow::Borrowed("python.exe"),
|
||||
Cow::Borrowed("python3.exe"),
|
||||
".exe",
|
||||
)
|
||||
} else {
|
||||
(Cow::Borrowed("python"), Cow::Borrowed("python3"), "")
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::Default => [Some(python3), Some(python), None, None],
|
||||
Self::Major(major) => [
|
||||
Some(Cow::Owned(format!("python{major}{extension}"))),
|
||||
Some(python),
|
||||
None,
|
||||
None,
|
||||
],
|
||||
Self::MajorMinor(major, minor) => [
|
||||
Some(Cow::Owned(format!("python{major}.{minor}{extension}"))),
|
||||
Some(Cow::Owned(format!("python{major}{extension}"))),
|
||||
Some(python),
|
||||
None,
|
||||
],
|
||||
Self::MajorMinorPatch(major, minor, patch) => [
|
||||
Some(Cow::Owned(format!(
|
||||
"python{major}.{minor}.{patch}{extension}",
|
||||
))),
|
||||
Some(Cow::Owned(format!("python{major}.{minor}{extension}"))),
|
||||
Some(Cow::Owned(format!("python{major}{extension}"))),
|
||||
Some(python),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_on_unsupported_python(interpreter: &Interpreter) {
|
||||
// Warn on usage with an unsupported Python version
|
||||
if interpreter.python_tuple() < (3, 8) {
|
||||
warn_user_once!(
|
||||
"uv is only compatible with Python 3.8+, found Python {}.",
|
||||
interpreter.python_version()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a matching Python or any fallback Python.
|
||||
///
|
||||
/// If no Python version is provided, we will use the first available interpreter.
|
||||
///
|
||||
/// If a Python version is provided, we will first try to find an exact match. If
|
||||
/// that cannot be found and a patch version was requested, we will look for a match
|
||||
/// without comparing the patch version number. If that cannot be found, we fall back to
|
||||
/// the first available version.
|
||||
///
|
||||
/// See [`Self::find_version`] for details on the precedence of Python lookup locations.
|
||||
#[instrument(skip_all, fields(?python_version))]
|
||||
pub fn find_best_python(
|
||||
python_version: Option<&PythonVersion>,
|
||||
system: bool,
|
||||
cache: &Cache,
|
||||
) -> Result<Interpreter, Error> {
|
||||
if let Some(python_version) = python_version {
|
||||
debug!(
|
||||
"Starting interpreter discovery for Python {}",
|
||||
python_version
|
||||
);
|
||||
} else {
|
||||
debug!("Starting interpreter discovery for active Python");
|
||||
}
|
||||
|
||||
// First, check for an exact match (or the first available version if no Python version was provided)
|
||||
if let Some(interpreter) = find_version(python_version, system, cache)? {
|
||||
warn_on_unsupported_python(&interpreter);
|
||||
return Ok(interpreter);
|
||||
}
|
||||
|
||||
if let Some(python_version) = python_version {
|
||||
// If that fails, and a specific patch version was requested try again allowing a
|
||||
// different patch version
|
||||
if python_version.patch().is_some() {
|
||||
if let Some(interpreter) =
|
||||
find_version(Some(&python_version.without_patch()), system, cache)?
|
||||
{
|
||||
warn_on_unsupported_python(&interpreter);
|
||||
return Ok(interpreter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a Python version was requested but cannot be fulfilled, just take any version
|
||||
if let Some(interpreter) = find_version(None, system, cache)? {
|
||||
return Ok(interpreter);
|
||||
}
|
||||
|
||||
Err(Error::PythonNotFound)
|
||||
}
|
||||
|
||||
/// Find a Python interpreter.
|
||||
///
|
||||
/// We check, in order, the following locations:
|
||||
///
|
||||
/// - `UV_DEFAULT_PYTHON`, which is set to the python interpreter when using `python -m uv`.
|
||||
/// - `VIRTUAL_ENV` and `CONDA_PREFIX`
|
||||
/// - A `.venv` folder
|
||||
/// - If a python version is given: Search `PATH` and `py --list-paths`, see `find_python`
|
||||
/// - `python3` (unix) or `python.exe` (windows)
|
||||
///
|
||||
/// If `UV_TEST_PYTHON_PATH` is set, we will not check for Python versions in the
|
||||
/// global PATH, instead we will search using the provided path. Virtual environments
|
||||
/// will still be respected.
|
||||
///
|
||||
/// If a version is provided and an interpreter cannot be found with the given version,
|
||||
/// we will return [`None`].
|
||||
fn find_version(
|
||||
python_version: Option<&PythonVersion>,
|
||||
system: bool,
|
||||
cache: &Cache,
|
||||
) -> Result<Option<Interpreter>, Error> {
|
||||
let version_matches = |interpreter: &Interpreter| -> bool {
|
||||
if let Some(python_version) = python_version {
|
||||
// If a patch version was provided, check for an exact match
|
||||
interpreter.satisfies(python_version)
|
||||
} else {
|
||||
// The version always matches if one was not provided
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the venv Python matches.
|
||||
if !system {
|
||||
if let Some(venv) = detect_virtualenv()? {
|
||||
let executable = virtualenv_python_executable(venv);
|
||||
let interpreter = Interpreter::query(executable, cache)?;
|
||||
|
||||
if version_matches(&interpreter) {
|
||||
return Ok(Some(interpreter));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Look for the requested version with by search for `python{major}.{minor}` in `PATH` on
|
||||
// Unix and `py --list-paths` on Windows.
|
||||
let interpreter = if let Some(python_version) = python_version {
|
||||
find_requested_python(&python_version.string, cache)?
|
||||
} else {
|
||||
try_find_default_python(cache)?
|
||||
};
|
||||
|
||||
if let Some(interpreter) = interpreter {
|
||||
debug_assert!(version_matches(&interpreter));
|
||||
Ok(Some(interpreter))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
mod windows {
|
||||
/// On Windows we might encounter the Windows Store proxy shim (enabled in:
|
||||
/// Settings/Apps/Advanced app settings/App execution aliases). When Python is _not_ installed
|
||||
/// via the Windows Store, but the proxy shim is enabled, then executing `python.exe` or
|
||||
/// `python3.exe` will redirect to the Windows Store installer.
|
||||
///
|
||||
/// We need to detect that these `python.exe` and `python3.exe` files are _not_ Python
|
||||
/// executables.
|
||||
///
|
||||
/// This method is taken from Rye:
|
||||
///
|
||||
/// > This is a pretty dumb way. We know how to parse this reparse point, but Microsoft
|
||||
/// > does not want us to do this as the format is unstable. So this is a best effort way.
|
||||
/// > we just hope that the reparse point has the python redirector in it, when it's not
|
||||
/// > pointing to a valid Python.
|
||||
///
|
||||
/// See: <https://github.com/astral-sh/rye/blob/b0e9eccf05fe4ff0ae7b0250a248c54f2d780b4d/rye/src/cli/shim.rs#L108>
|
||||
#[cfg(windows)]
|
||||
pub(super) fn is_windows_store_shim(path: &std::path::Path) -> bool {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
use std::os::windows::prelude::OsStrExt;
|
||||
use winapi::um::fileapi::{CreateFileW, OPEN_EXISTING};
|
||||
use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
|
||||
use winapi::um::ioapiset::DeviceIoControl;
|
||||
use winapi::um::winbase::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT};
|
||||
use winapi::um::winioctl::FSCTL_GET_REPARSE_POINT;
|
||||
use winapi::um::winnt::{FILE_ATTRIBUTE_REPARSE_POINT, MAXIMUM_REPARSE_DATA_BUFFER_SIZE};
|
||||
|
||||
// The path must be absolute.
|
||||
if !path.is_absolute() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The path must point to something like:
|
||||
// `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python3.exe`
|
||||
let mut components = path.components().rev();
|
||||
|
||||
// Ex) `python.exe` or `python3.exe`
|
||||
if !components
|
||||
.next()
|
||||
.and_then(|component| component.as_os_str().to_str())
|
||||
.is_some_and(|component| component == "python.exe" || component == "python3.exe")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ex) `WindowsApps`
|
||||
if !components
|
||||
.next()
|
||||
.is_some_and(|component| component.as_os_str() == "WindowsApps")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ex) `Microsoft`
|
||||
if !components
|
||||
.next()
|
||||
.is_some_and(|component| component.as_os_str() == "Microsoft")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The file is only relevant if it's a reparse point.
|
||||
let Ok(md) = fs_err::symlink_metadata(path) else {
|
||||
return false;
|
||||
};
|
||||
if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut path_encoded = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// SAFETY: The path is null-terminated.
|
||||
#[allow(unsafe_code)]
|
||||
let reparse_handle = unsafe {
|
||||
CreateFileW(
|
||||
path_encoded.as_mut_ptr(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if reparse_handle == INVALID_HANDLE_VALUE {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
|
||||
let mut bytes_returned = 0;
|
||||
|
||||
// SAFETY: The buffer is large enough to hold the reparse point.
|
||||
#[allow(unsafe_code, clippy::cast_possible_truncation)]
|
||||
let success = unsafe {
|
||||
DeviceIoControl(
|
||||
reparse_handle,
|
||||
FSCTL_GET_REPARSE_POINT,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
buf.as_mut_ptr().cast(),
|
||||
buf.len() as u32 * 2,
|
||||
&mut bytes_returned,
|
||||
std::ptr::null_mut(),
|
||||
) != 0
|
||||
};
|
||||
|
||||
// SAFETY: The handle is valid.
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
CloseHandle(reparse_handle);
|
||||
}
|
||||
|
||||
// If the operation failed, assume it's not a reparse point.
|
||||
if !success {
|
||||
return false;
|
||||
}
|
||||
|
||||
let reparse_point = String::from_utf16_lossy(&buf[..bytes_returned as usize]);
|
||||
reparse_point.contains("\\AppInstallerPythonRedirector.exe")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Debug;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use itertools::Itertools;
|
||||
|
||||
use uv_cache::Cache;
|
||||
|
||||
use crate::{find_requested_python, Error};
|
||||
|
||||
fn format_err<T: Debug>(err: Result<T, Error>) -> String {
|
||||
anyhow::Error::new(err.unwrap_err())
|
||||
.chain()
|
||||
.join("\n Caused by: ")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(windows), ignore)]
|
||||
fn no_such_python_path() {
|
||||
let cache = Cache::temp().unwrap().init().unwrap();
|
||||
let result = find_requested_python(r"C:\does\not\exists\python3.12", &cache)
|
||||
.unwrap()
|
||||
.ok_or(Error::RequestedPythonNotFound(
|
||||
r"C:\does\not\exists\python3.12".to_string(),
|
||||
));
|
||||
assert_snapshot!(
|
||||
format_err(result),
|
||||
@"Failed to locate Python interpreter at: `C:\\does\\not\\exists\\python3.12`"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use itertools::Itertools;
|
||||
|
||||
use uv_cache::Cache;
|
||||
|
||||
use crate::find_python::find_requested_python;
|
||||
use crate::Error;
|
||||
|
||||
fn format_err<T: std::fmt::Debug>(err: Result<T, Error>) -> String {
|
||||
anyhow::Error::new(err.unwrap_err())
|
||||
.chain()
|
||||
.join("\n Caused by: ")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(unix), ignore)]
|
||||
fn no_such_python_version() {
|
||||
let cache = Cache::temp().unwrap().init().unwrap();
|
||||
let request = "3.1000";
|
||||
let result = find_requested_python(request, &cache)
|
||||
.unwrap()
|
||||
.ok_or(Error::NoSuchPython(request.to_string()));
|
||||
assert_snapshot!(
|
||||
format_err(result),
|
||||
@"No Python 3.1000 in `PATH`. Is Python 3.1000 installed?"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(unix), ignore)]
|
||||
fn no_such_python_binary() {
|
||||
let cache = Cache::temp().unwrap().init().unwrap();
|
||||
let request = "python3.1000";
|
||||
let result = find_requested_python(request, &cache)
|
||||
.unwrap()
|
||||
.ok_or(Error::NoSuchPython(request.to_string()));
|
||||
assert_snapshot!(
|
||||
format_err(result),
|
||||
@"No Python python3.1000 in `PATH`. Is Python python3.1000 installed?"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(unix), ignore)]
|
||||
fn no_such_python_path() {
|
||||
let cache = Cache::temp().unwrap().init().unwrap();
|
||||
let result = find_requested_python("/does/not/exists/python3.12", &cache)
|
||||
.unwrap()
|
||||
.ok_or(Error::RequestedPythonNotFound(
|
||||
"/does/not/exists/python3.12".to_string(),
|
||||
));
|
||||
assert_snapshot!(
|
||||
format_err(result), @"Failed to locate Python interpreter at: `/does/not/exists/python3.12`");
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ pub enum ImplementationName {
|
|||
}
|
||||
|
||||
impl ImplementationName {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn iter() -> impl Iterator<Item = &'static ImplementationName> {
|
||||
static NAMES: &[ImplementationName] = &[ImplementationName::Cpython];
|
||||
NAMES.iter()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::process::{Command, ExitStatus};
|
||||
|
||||
use configparser::ini::Ini;
|
||||
use fs_err as fs;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, warn};
|
||||
use thiserror::Error;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use cache_key::digest;
|
||||
use install_wheel_rs::Layout;
|
||||
|
|
@ -18,7 +20,7 @@ use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp};
|
|||
use uv_fs::{write_atomic_sync, PythonExt, Simplified};
|
||||
|
||||
use crate::pointer_size::PointerSize;
|
||||
use crate::{Error, PythonVersion, Target, VirtualEnvironment};
|
||||
use crate::{PythonVersion, Target, VirtualEnvironment};
|
||||
|
||||
/// A Python executable and its associated platform markers.
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -407,6 +409,41 @@ impl ExternallyManaged {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Failed to query Python interpreter at `{path}`")]
|
||||
SpawnFailed {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
err: io::Error,
|
||||
},
|
||||
#[error("Querying Python at `{}` did not return the expected data\n{err}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---", path.display())]
|
||||
UnexpectedResponse {
|
||||
err: serde_json::Error,
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
#[error("Querying Python at `{}` failed with exit status {code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---", path.display())]
|
||||
StatusCode {
|
||||
code: ExitStatus,
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
path: PathBuf,
|
||||
},
|
||||
#[error("Can't use Python at `{path}`")]
|
||||
QueryScript {
|
||||
#[source]
|
||||
err: InterpreterInfoError,
|
||||
path: PathBuf,
|
||||
},
|
||||
#[error("Failed to write to cache")]
|
||||
Encode(#[from] rmp_serde::encode::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "result", rename_all = "lowercase")]
|
||||
enum InterpreterInfoResult {
|
||||
|
|
@ -423,6 +460,8 @@ pub enum InterpreterInfoError {
|
|||
UnknownOperatingSystem { operating_system: String },
|
||||
#[error("Python {python_version} is not supported. Please use Python 3.8 or newer.")]
|
||||
UnsupportedPythonVersion { python_version: String },
|
||||
#[error("Python executable does not support `-I` flag. Please use Python 3.8 or newer.")]
|
||||
UnsupportedPython,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
|
|
@ -460,41 +499,54 @@ impl InterpreterInfo {
|
|||
.arg("-c")
|
||||
.arg(script)
|
||||
.output()
|
||||
.map_err(|err| Error::PythonSubcommandLaunch {
|
||||
interpreter: interpreter.to_path_buf(),
|
||||
.map_err(|err| Error::SpawnFailed {
|
||||
path: interpreter.to_path_buf(),
|
||||
err,
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(Error::PythonSubcommandOutput {
|
||||
message: format!(
|
||||
"Querying Python at `{}` failed with status {}",
|
||||
interpreter.display(),
|
||||
output.status,
|
||||
),
|
||||
exit_code: output.status,
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
|
||||
// If the Python version is too old, we may not even be able to invoke the query script
|
||||
if stderr.contains("Unknown option: -I") {
|
||||
return Err(Error::QueryScript {
|
||||
err: InterpreterInfoError::UnsupportedPython,
|
||||
path: interpreter.to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
return Err(Error::StatusCode {
|
||||
code: output.status,
|
||||
stderr,
|
||||
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
|
||||
path: interpreter.to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
let result: InterpreterInfoResult =
|
||||
serde_json::from_slice(&output.stdout).map_err(|err| {
|
||||
Error::PythonSubcommandOutput {
|
||||
message: format!(
|
||||
"Querying Python at `{}` did not return the expected data: {err}",
|
||||
interpreter.display(),
|
||||
),
|
||||
exit_code: output.status,
|
||||
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
|
||||
// If the Python version is too old, we may not even be able to invoke the query script
|
||||
if stderr.contains("Unknown option: -I") {
|
||||
Error::QueryScript {
|
||||
err: InterpreterInfoError::UnsupportedPython,
|
||||
path: interpreter.to_path_buf(),
|
||||
}
|
||||
} else {
|
||||
Error::UnexpectedResponse {
|
||||
err,
|
||||
stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
|
||||
stderr,
|
||||
path: interpreter.to_path_buf(),
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
match result {
|
||||
InterpreterInfoResult::Error(err) => Err(Error::QueryScript {
|
||||
err,
|
||||
interpreter: interpreter.to_path_buf(),
|
||||
path: interpreter.to_path_buf(),
|
||||
}),
|
||||
InterpreterInfoResult::Success(data) => Ok(*data),
|
||||
}
|
||||
|
|
@ -557,7 +609,7 @@ impl InterpreterInfo {
|
|||
match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
|
||||
Ok(cached) => {
|
||||
if cached.timestamp == modified {
|
||||
debug!(
|
||||
trace!(
|
||||
"Cached interpreter info for Python {}, skipping probing: {}",
|
||||
cached.data.markers.python_full_version(),
|
||||
executable.user_display()
|
||||
|
|
@ -565,14 +617,14 @@ impl InterpreterInfo {
|
|||
return Ok(cached.data);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Ignoring stale cached markers for: {}",
|
||||
trace!(
|
||||
"Ignoring stale interpreter markers for: {}",
|
||||
executable.user_display()
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Broken cache entry at {}, removing: {err}",
|
||||
"Broken interpreter cache entry at {}, removing: {err}",
|
||||
cache_entry.path().user_display()
|
||||
);
|
||||
let _ = fs_err::remove_file(cache_entry.path());
|
||||
|
|
@ -582,10 +634,13 @@ impl InterpreterInfo {
|
|||
}
|
||||
|
||||
// Otherwise, run the Python script.
|
||||
debug!("Probing interpreter info for: {}", executable.display());
|
||||
trace!(
|
||||
"Querying interpreter executable at {}",
|
||||
executable.display()
|
||||
);
|
||||
let info = Self::query(executable, cache)?;
|
||||
debug!(
|
||||
"Found Python {} for: {}",
|
||||
trace!(
|
||||
"Found Python {} at {}",
|
||||
info.markers.python_full_version(),
|
||||
executable.display()
|
||||
);
|
||||
|
|
@ -687,6 +742,7 @@ mod tests {
|
|||
"##},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
fs::set_permissions(
|
||||
&mocked_interpreter,
|
||||
std::os::unix::fs::PermissionsExt::from_mode(0o770),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -46,6 +46,8 @@ pub enum Error {
|
|||
#[source]
|
||||
err: io::Error,
|
||||
},
|
||||
#[error("failed to parse toolchain directory name: {0}")]
|
||||
NameError(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -1,38 +1,77 @@
|
|||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::managed::downloads::Error;
|
||||
use crate::{
|
||||
platform::{Arch, Libc, Os},
|
||||
python_version::PythonVersion,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use tracing::debug;
|
||||
|
||||
use uv_fs::Simplified;
|
||||
|
||||
use crate::managed::downloads::Error;
|
||||
use crate::platform::{Arch, Libc, Os};
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
/// The directory where Python toolchains we install are stored.
|
||||
pub static TOOLCHAIN_DIRECTORY: Lazy<Option<PathBuf>> = Lazy::new(|| {
|
||||
std::env::var_os("UV_BOOTSTRAP_DIR").map_or(
|
||||
std::env::var_os("CARGO_MANIFEST_DIR").map(|manifest_dir| {
|
||||
Path::new(&manifest_dir)
|
||||
.parent()
|
||||
.expect("CARGO_MANIFEST_DIR should be nested in workspace")
|
||||
.parent()
|
||||
.expect("CARGO_MANIFEST_DIR should be doubly nested in workspace")
|
||||
.join("bin")
|
||||
}),
|
||||
|bootstrap_dir| Some(PathBuf::from(bootstrap_dir)),
|
||||
)
|
||||
});
|
||||
pub static TOOLCHAIN_DIRECTORY: Lazy<Option<PathBuf>> =
|
||||
Lazy::new(|| std::env::var_os("UV_BOOTSTRAP_DIR").map(PathBuf::from));
|
||||
|
||||
pub fn toolchains_for_current_platform() -> Result<impl Iterator<Item = Toolchain>, Error> {
|
||||
let platform_key = platform_key_from_env()?;
|
||||
let iter = toolchain_directories()?
|
||||
.into_iter()
|
||||
// Sort "newer" versions of Python first
|
||||
.rev()
|
||||
.filter_map(move |path| {
|
||||
if path
|
||||
.file_name()
|
||||
.map(OsStr::to_string_lossy)
|
||||
.is_some_and(|filename| filename.ends_with(&platform_key))
|
||||
{
|
||||
Toolchain::new(path.clone())
|
||||
.inspect_err(|err| {
|
||||
debug!(
|
||||
"Ignoring invalid toolchain directory {}: {err}",
|
||||
path.user_display()
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Ok(iter)
|
||||
}
|
||||
|
||||
/// An installed Python toolchain.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Toolchain {
|
||||
/// The path to the top-level directory of the installed toolchain.
|
||||
path: PathBuf,
|
||||
python_version: PythonVersion,
|
||||
}
|
||||
|
||||
impl Toolchain {
|
||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
||||
let python_version = PythonVersion::from_str(
|
||||
path.file_name()
|
||||
.ok_or(Error::NameError("No directory name".to_string()))?
|
||||
.to_str()
|
||||
.ok_or(Error::NameError("Name not a valid string".to_string()))?
|
||||
.split('-')
|
||||
.nth(1)
|
||||
.ok_or(Error::NameError(
|
||||
"Not enough `-` separarated values".to_string(),
|
||||
))?,
|
||||
)
|
||||
.map_err(|err| Error::NameError(format!("Name has invalid Python version: {err}")))?;
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
python_version,
|
||||
})
|
||||
}
|
||||
pub fn executable(&self) -> PathBuf {
|
||||
if cfg!(windows) {
|
||||
self.path.join("install").join("python.exe")
|
||||
|
|
@ -42,8 +81,18 @@ impl Toolchain {
|
|||
unimplemented!("Only Windows and Unix systems are supported.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn python_version(&self) -> &PythonVersion {
|
||||
&self.python_version
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the directories in the toolchain directory.
|
||||
///
|
||||
/// Toolchain directories are sorted descending by name, such that we get deterministic
|
||||
/// ordering across platforms. This also results in newer Python versions coming first,
|
||||
/// but should not be relied on — instead the toolchains should be sorted later by
|
||||
/// the parsed Python version.
|
||||
fn toolchain_directories() -> Result<BTreeSet<PathBuf>, Error> {
|
||||
let Some(toolchain_dir) = TOOLCHAIN_DIRECTORY.as_ref() else {
|
||||
return Ok(BTreeSet::default());
|
||||
|
|
@ -101,7 +150,14 @@ pub fn toolchains_for_version(version: &PythonVersion) -> Result<Vec<Toolchain>,
|
|||
&& filename.ends_with(&platform_key)
|
||||
})
|
||||
{
|
||||
Some(Toolchain { path })
|
||||
Toolchain::new(path.clone())
|
||||
.inspect_err(|err| {
|
||||
debug!(
|
||||
"Ignoring invalid toolchain directory {}: {err}",
|
||||
path.user_display()
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
pub use crate::managed::downloads::{DownloadResult, Error, PythonDownload, PythonDownloadRequest};
|
||||
pub use crate::managed::find::{toolchains_for_version, Toolchain, TOOLCHAIN_DIRECTORY};
|
||||
pub use crate::managed::find::{
|
||||
toolchains_for_current_platform, toolchains_for_version, Toolchain, TOOLCHAIN_DIRECTORY,
|
||||
};
|
||||
|
||||
mod downloads;
|
||||
mod find;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ pub(crate) struct PyListPath {
|
|||
pub(crate) executable_path: PathBuf,
|
||||
}
|
||||
|
||||
/// An error was encountered when using the `py` launcher on Windows.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
|
||||
|
|
@ -91,36 +92,3 @@ pub(crate) fn py_list_paths() -> Result<Vec<PyListPath>, Error> {
|
|||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Debug;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use itertools::Itertools;
|
||||
|
||||
use uv_cache::Cache;
|
||||
|
||||
use crate::{find_requested_python, Error};
|
||||
|
||||
fn format_err<T: Debug>(err: Result<T, Error>) -> String {
|
||||
anyhow::Error::new(err.unwrap_err())
|
||||
.chain()
|
||||
.join("\n Caused by: ")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(windows), ignore)]
|
||||
fn no_such_python_path() {
|
||||
let cache = Cache::temp().unwrap().init().unwrap();
|
||||
let result = find_requested_python(r"C:\does\not\exists\python3.12", &cache)
|
||||
.unwrap()
|
||||
.ok_or(Error::RequestedPythonNotFound(
|
||||
r"C:\does\not\exists\python3.12".to_string(),
|
||||
));
|
||||
assert_snapshot!(
|
||||
format_err(result),
|
||||
@"Failed to locate Python interpreter at: `C:\\does\\not\\exists\\python3.12`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,15 +37,8 @@ pub enum Error {
|
|||
MissingPyVenvCfg(PathBuf),
|
||||
#[error("Broken virtualenv `{0}`: `pyvenv.cfg` could not be parsed")]
|
||||
ParsePyVenvCfg(PathBuf, #[source] io::Error),
|
||||
}
|
||||
|
||||
/// Locate the current virtual environment.
|
||||
pub(crate) fn detect_virtualenv() -> Result<Option<PathBuf>, Error> {
|
||||
let from_env = virtualenv_from_env();
|
||||
if from_env.is_some() {
|
||||
return Ok(from_env);
|
||||
}
|
||||
virtualenv_from_working_dir()
|
||||
#[error(transparent)]
|
||||
IO(#[from] io::Error),
|
||||
}
|
||||
|
||||
/// Locate an active virtual environment by inspecting environment variables.
|
||||
|
|
@ -54,7 +47,7 @@ pub(crate) fn detect_virtualenv() -> Result<Option<PathBuf>, Error> {
|
|||
pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
|
||||
if let Some(dir) = env::var_os("VIRTUAL_ENV").filter(|value| !value.is_empty()) {
|
||||
info!(
|
||||
"Found a virtualenv through VIRTUAL_ENV at: {}",
|
||||
"Found active virtual environment (via VIRTUAL_ENV) at: {}",
|
||||
Path::new(&dir).display()
|
||||
);
|
||||
return Some(PathBuf::from(dir));
|
||||
|
|
@ -62,7 +55,7 @@ pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
|
|||
|
||||
if let Some(dir) = env::var_os("CONDA_PREFIX").filter(|value| !value.is_empty()) {
|
||||
info!(
|
||||
"Found a virtualenv through CONDA_PREFIX at: {}",
|
||||
"Found active virtual environment (via CONDA_PREFIX) at: {}",
|
||||
Path::new(&dir).display()
|
||||
);
|
||||
return Some(PathBuf::from(dir));
|
||||
|
|
@ -77,12 +70,12 @@ pub(crate) fn virtualenv_from_env() -> Option<PathBuf> {
|
|||
/// directory is itself a virtual environment (or a subdirectory of a virtual environment), the
|
||||
/// containing virtual environment is returned.
|
||||
pub(crate) fn virtualenv_from_working_dir() -> Result<Option<PathBuf>, Error> {
|
||||
let current_dir = env::current_dir().expect("Failed to detect current directory");
|
||||
let current_dir = crate::current_dir()?;
|
||||
|
||||
for dir in current_dir.ancestors() {
|
||||
// If we're _within_ a virtualenv, return it.
|
||||
if dir.join("pyvenv.cfg").is_file() {
|
||||
debug!("Found a virtualenv at: {}", dir.display());
|
||||
debug!("Found a virtual environment at: {}", dir.display());
|
||||
return Ok(Some(dir.to_path_buf()));
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +85,7 @@ pub(crate) fn virtualenv_from_working_dir() -> Result<Option<PathBuf>, Error> {
|
|||
if !dot_venv.join("pyvenv.cfg").is_file() {
|
||||
return Err(Error::MissingPyVenvCfg(dot_venv));
|
||||
}
|
||||
debug!("Found a virtualenv named .venv at: {}", dot_venv.display());
|
||||
debug!("Found a virtual environment at: {}", dot_venv.display());
|
||||
return Ok(Some(dot_venv));
|
||||
}
|
||||
}
|
||||
|
|
@ -105,9 +98,9 @@ pub(crate) fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
|
|||
let venv = venv.as_ref();
|
||||
if cfg!(windows) {
|
||||
// Search for `python.exe` in the `Scripts` directory.
|
||||
let executable = venv.join("Scripts").join("python.exe");
|
||||
if executable.exists() {
|
||||
return executable;
|
||||
let default_executable = venv.join("Scripts").join("python.exe");
|
||||
if default_executable.exists() {
|
||||
return default_executable;
|
||||
}
|
||||
|
||||
// Apparently, Python installed via msys2 on Windows _might_ produce a POSIX-like layout.
|
||||
|
|
@ -118,7 +111,13 @@ pub(crate) fn virtualenv_python_executable(venv: impl AsRef<Path>) -> PathBuf {
|
|||
}
|
||||
|
||||
// Fallback for Conda environments.
|
||||
venv.join("python.exe")
|
||||
let executable = venv.join("python.exe");
|
||||
if executable.exists() {
|
||||
return executable;
|
||||
}
|
||||
|
||||
// If none of these exist, return the standard location
|
||||
default_executable
|
||||
} else {
|
||||
// Search for `python` in the `bin` directory.
|
||||
venv.join("bin").join("python")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use uv_configuration::{
|
|||
BuildKind, Concurrency, Constraints, NoBinary, NoBuild, Overrides, SetupPyStrategy,
|
||||
};
|
||||
use uv_distribution::DistributionDatabase;
|
||||
use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment};
|
||||
use uv_interpreter::{find_default_interpreter, Interpreter, PythonEnvironment};
|
||||
use uv_resolver::{
|
||||
DisplayResolutionGraph, ExcludeNewer, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options,
|
||||
OptionsBuilder, PreReleaseMode, Preference, PythonRequirement, ResolutionGraph, ResolutionMode,
|
||||
|
|
@ -124,10 +124,13 @@ async fn resolve(
|
|||
tags: &Tags,
|
||||
) -> Result<ResolutionGraph> {
|
||||
let cache = Cache::temp().unwrap().init().unwrap();
|
||||
let real_interpreter = find_default_python(&cache).expect("Expected a python to be installed");
|
||||
let client = RegistryClientBuilder::new(cache).build();
|
||||
let flat_index = FlatIndex::default();
|
||||
let index = InMemoryIndex::default();
|
||||
let real_interpreter = find_default_interpreter(&Cache::temp().unwrap())
|
||||
.unwrap()
|
||||
.expect("Python should be installed")
|
||||
.into_interpreter();
|
||||
let interpreter = Interpreter::artificial(real_interpreter.platform().clone(), markers.clone());
|
||||
let python_requirement = PythonRequirement::from_marker_environment(&interpreter, markers);
|
||||
let cache = Cache::temp().unwrap().init().unwrap();
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ mod bare;
|
|||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
IO(#[from] io::Error),
|
||||
#[error("Failed to determine python interpreter to use")]
|
||||
InterpreterError(#[from] uv_interpreter::Error),
|
||||
#[error("Failed to determine Python interpreter to use")]
|
||||
Discovery(#[from] uv_interpreter::DiscoveryError),
|
||||
#[error("Failed to determine Python interpreter to use")]
|
||||
InterpreterNotFound(#[from] uv_interpreter::InterpreterNotFound),
|
||||
#[error(transparent)]
|
||||
Platform(#[from] PlatformError),
|
||||
#[error("Could not find a suitable Python executable for the virtual environment based on the interpreter: {0}")]
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use tracing::debug;
|
|||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::{Diagnostic, SitePackages};
|
||||
use uv_interpreter::PythonEnvironment;
|
||||
use uv_interpreter::{PythonEnvironment, SystemPython};
|
||||
|
||||
use crate::commands::{elapsed, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
|
|
@ -24,19 +24,12 @@ pub(crate) fn pip_check(
|
|||
let start = Instant::now();
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
let venv = if let Some(python) = python {
|
||||
PythonEnvironment::from_requested_python(python, cache)?
|
||||
} else if system {
|
||||
PythonEnvironment::from_default_python(cache)?
|
||||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
match PythonEnvironment::from_virtualenv(cache) {
|
||||
Ok(venv) => venv,
|
||||
Err(uv_interpreter::Error::VenvNotFound) => {
|
||||
PythonEnvironment::from_default_python(cache)?
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
SystemPython::Allowed
|
||||
};
|
||||
let venv = PythonEnvironment::find(python, system, cache)?;
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
|
|
|
|||
|
|
@ -35,8 +35,11 @@ use uv_dispatch::BuildDispatch;
|
|||
use uv_distribution::DistributionDatabase;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::Downloader;
|
||||
use uv_interpreter::PythonVersion;
|
||||
use uv_interpreter::{find_best_python, find_requested_python, PythonEnvironment};
|
||||
use uv_interpreter::{
|
||||
find_best_interpreter, find_interpreter, InterpreterRequest, PythonEnvironment, SystemPython,
|
||||
VersionRequest,
|
||||
};
|
||||
use uv_interpreter::{PythonVersion, SourceSelector};
|
||||
use uv_normalize::{ExtraName, PackageName};
|
||||
use uv_requirements::{
|
||||
upgrade::read_lockfile, ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver,
|
||||
|
|
@ -160,12 +163,26 @@ pub(crate) async fn pip_compile(
|
|||
}
|
||||
|
||||
// Find an interpreter to use for building distributions
|
||||
let interpreter = if let Some(python) = python.as_ref() {
|
||||
find_requested_python(python, &cache)?
|
||||
.ok_or_else(|| uv_interpreter::Error::RequestedPythonNotFound(python.to_string()))?
|
||||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
find_best_python(python_version.as_ref(), system, &cache)?
|
||||
SystemPython::Allowed
|
||||
};
|
||||
let interpreter = if let Some(python) = python.as_ref() {
|
||||
let request = InterpreterRequest::parse(python);
|
||||
let sources = SourceSelector::from_env(system);
|
||||
find_interpreter(&request, &sources, &cache)??
|
||||
} else {
|
||||
let request = if let Some(version) = python_version.as_ref() {
|
||||
// TODO(zanieb): We should consolidate `VersionRequest` and `PythonVersion`
|
||||
InterpreterRequest::Version(VersionRequest::from(version))
|
||||
} else {
|
||||
InterpreterRequest::Version(VersionRequest::Default)
|
||||
};
|
||||
find_best_interpreter(&request, system, &cache)??
|
||||
}
|
||||
.into_interpreter();
|
||||
|
||||
debug!(
|
||||
"Using Python {} interpreter at {} for builds",
|
||||
interpreter.python_version(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use distribution_types::{InstalledDist, Name};
|
|||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_interpreter::PythonEnvironment;
|
||||
use uv_interpreter::{PythonEnvironment, SystemPython};
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::printer::Printer;
|
||||
|
|
@ -24,19 +24,12 @@ pub(crate) fn pip_freeze(
|
|||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
// Detect the current Python interpreter.
|
||||
let venv = if let Some(python) = python {
|
||||
PythonEnvironment::from_requested_python(python, cache)?
|
||||
} else if system {
|
||||
PythonEnvironment::from_default_python(cache)?
|
||||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
match PythonEnvironment::from_virtualenv(cache) {
|
||||
Ok(venv) => venv,
|
||||
Err(uv_interpreter::Error::VenvNotFound) => {
|
||||
PythonEnvironment::from_default_python(cache)?
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
SystemPython::Allowed
|
||||
};
|
||||
let venv = PythonEnvironment::find(python, system, cache)?;
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ use uv_dispatch::BuildDispatch;
|
|||
use uv_distribution::DistributionDatabase;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::{Downloader, Plan, Planner, ResolvedEditable, SatisfiesResult, SitePackages};
|
||||
use uv_interpreter::{Interpreter, PythonEnvironment, PythonVersion, Target};
|
||||
use uv_interpreter::{Interpreter, PythonEnvironment, PythonVersion, SystemPython, Target};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_requirements::{
|
||||
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
|
||||
|
|
@ -125,13 +125,13 @@ pub(crate) async fn pip_install(
|
|||
.await?;
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
let venv = if let Some(python) = python.as_ref() {
|
||||
PythonEnvironment::from_requested_python(python, &cache)?
|
||||
} else if system {
|
||||
PythonEnvironment::from_default_python(&cache)?
|
||||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
PythonEnvironment::from_virtualenv(&cache)?
|
||||
SystemPython::Disallowed
|
||||
};
|
||||
let venv = PythonEnvironment::find(python.as_deref(), system, &cache)?;
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
venv.interpreter().python_version(),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use distribution_types::{InstalledDist, Name};
|
|||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_interpreter::PythonEnvironment;
|
||||
use uv_interpreter::{PythonEnvironment, SystemPython};
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
|
|
@ -33,19 +33,12 @@ pub(crate) fn pip_list(
|
|||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
// Detect the current Python interpreter.
|
||||
let venv = if let Some(python) = python {
|
||||
PythonEnvironment::from_requested_python(python, cache)?
|
||||
} else if system {
|
||||
PythonEnvironment::from_default_python(cache)?
|
||||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
match PythonEnvironment::from_virtualenv(cache) {
|
||||
Ok(venv) => venv,
|
||||
Err(uv_interpreter::Error::VenvNotFound) => {
|
||||
PythonEnvironment::from_default_python(cache)?
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
SystemPython::Allowed
|
||||
};
|
||||
let venv = PythonEnvironment::find(python, system, cache)?;
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use distribution_types::Name;
|
|||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::SitePackages;
|
||||
use uv_interpreter::PythonEnvironment;
|
||||
use uv_interpreter::{PythonEnvironment, SystemPython};
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
use crate::commands::ExitStatus;
|
||||
|
|
@ -39,19 +39,12 @@ pub(crate) fn pip_show(
|
|||
}
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
let venv = if let Some(python) = python {
|
||||
PythonEnvironment::from_requested_python(python, cache)?
|
||||
} else if system {
|
||||
PythonEnvironment::from_default_python(cache)?
|
||||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
match PythonEnvironment::from_virtualenv(cache) {
|
||||
Ok(venv) => venv,
|
||||
Err(uv_interpreter::Error::VenvNotFound) => {
|
||||
PythonEnvironment::from_default_python(cache)?
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
SystemPython::Allowed
|
||||
};
|
||||
let venv = PythonEnvironment::find(python, system, cache)?;
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use uv_dispatch::BuildDispatch;
|
|||
use uv_distribution::DistributionDatabase;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::{Downloader, Plan, Planner, SitePackages};
|
||||
use uv_interpreter::{PythonEnvironment, PythonVersion, Target};
|
||||
use uv_interpreter::{PythonEnvironment, PythonVersion, SystemPython, Target};
|
||||
use uv_requirements::{
|
||||
ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
|
||||
SourceTreeResolver,
|
||||
|
|
@ -101,13 +101,13 @@ pub(crate) async fn pip_sync(
|
|||
}
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
let venv = if let Some(python) = python.as_ref() {
|
||||
PythonEnvironment::from_requested_python(python, &cache)?
|
||||
} else if system {
|
||||
PythonEnvironment::from_default_python(&cache)?
|
||||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
PythonEnvironment::from_virtualenv(&cache)?
|
||||
SystemPython::Disallowed
|
||||
};
|
||||
let venv = PythonEnvironment::find(python.as_deref(), system, &cache)?;
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
venv.interpreter().python_version(),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use uv_cache::Cache;
|
|||
use uv_client::{BaseClientBuilder, Connectivity};
|
||||
use uv_configuration::{KeyringProviderType, PreviewMode};
|
||||
use uv_fs::Simplified;
|
||||
use uv_interpreter::{PythonEnvironment, Target};
|
||||
use uv_interpreter::{PythonEnvironment, SystemPython, Target};
|
||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
|
||||
use crate::commands::{elapsed, ExitStatus};
|
||||
|
|
@ -43,13 +43,13 @@ pub(crate) async fn pip_uninstall(
|
|||
RequirementsSpecification::from_simple_sources(sources, &client_builder, preview).await?;
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
let venv = if let Some(python) = python.as_ref() {
|
||||
PythonEnvironment::from_requested_python(python, &cache)?
|
||||
} else if system {
|
||||
PythonEnvironment::from_default_python(&cache)?
|
||||
let system = if system {
|
||||
SystemPython::Required
|
||||
} else {
|
||||
PythonEnvironment::from_virtualenv(&cache)?
|
||||
SystemPython::Disallowed
|
||||
};
|
||||
let venv = PythonEnvironment::find(python.as_deref(), system, &cache)?;
|
||||
|
||||
debug!(
|
||||
"Using Python {} environment at {}",
|
||||
venv.interpreter().python_version(),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ use uv_dispatch::BuildDispatch;
|
|||
use uv_distribution::DistributionDatabase;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::{Downloader, Plan, Planner, SatisfiesResult, SitePackages};
|
||||
use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment};
|
||||
use uv_interpreter::{find_default_interpreter, Interpreter, PythonEnvironment};
|
||||
use uv_requirements::{
|
||||
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, ProjectWorkspace,
|
||||
RequirementsSource, RequirementsSpecification, SourceTreeResolver,
|
||||
|
|
@ -88,9 +88,12 @@ pub(crate) fn init(
|
|||
// TODO(charlie): If the environment isn't compatible with `--python`, recreate it.
|
||||
match PythonEnvironment::from_root(&venv, cache) {
|
||||
Ok(venv) => Ok(venv),
|
||||
Err(uv_interpreter::Error::VenvDoesNotExist(_)) => {
|
||||
Err(uv_interpreter::Error::NotFound(_)) => {
|
||||
// TODO(charlie): Respect `--python`; if unset, respect `Requires-Python`.
|
||||
let interpreter = find_default_python(cache)?;
|
||||
let interpreter = find_default_interpreter(cache)
|
||||
.map_err(uv_interpreter::Error::from)?
|
||||
.map_err(uv_interpreter::Error::from)?
|
||||
.into_interpreter();
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use tracing::debug;
|
|||
|
||||
use uv_cache::Cache;
|
||||
use uv_configuration::PreviewMode;
|
||||
use uv_interpreter::PythonEnvironment;
|
||||
use uv_interpreter::{PythonEnvironment, SystemPython};
|
||||
use uv_requirements::{ProjectWorkspace, RequirementsSource};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
|
|
@ -76,7 +76,8 @@ pub(crate) async fn run(
|
|||
let interpreter = if let Some(project_env) = &project_env {
|
||||
project_env.interpreter().clone()
|
||||
} else if let Some(python) = python.as_ref() {
|
||||
PythonEnvironment::from_requested_python(python, cache)?.into_interpreter()
|
||||
PythonEnvironment::from_requested_python(python, SystemPython::Allowed, cache)?
|
||||
.into_interpreter()
|
||||
} else {
|
||||
PythonEnvironment::from_default_python(cache)?.into_interpreter()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ use uv_configuration::{Concurrency, KeyringProviderType};
|
|||
use uv_configuration::{ConfigSettings, IndexStrategy, NoBinary, NoBuild, SetupPyStrategy};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_fs::Simplified;
|
||||
use uv_interpreter::{find_default_python, find_requested_python, Error};
|
||||
use uv_interpreter::{
|
||||
find_default_interpreter, find_interpreter, InterpreterRequest, SourceSelector,
|
||||
};
|
||||
use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, OptionsBuilder};
|
||||
use uv_types::{BuildContext, BuildIsolation, HashStrategy, InFlight};
|
||||
|
||||
|
|
@ -116,14 +118,16 @@ async fn venv_impl(
|
|||
printer: Printer,
|
||||
) -> miette::Result<ExitStatus> {
|
||||
// Locate the Python interpreter.
|
||||
let interpreter = if let Some(python_request) = python_request {
|
||||
find_requested_python(python_request, cache)
|
||||
.into_diagnostic()?
|
||||
.ok_or(Error::NoSuchPython(python_request.to_string()))
|
||||
.into_diagnostic()?
|
||||
let interpreter = if let Some(python) = python_request.as_ref() {
|
||||
let request = InterpreterRequest::parse(python);
|
||||
let sources = SourceSelector::from_env(uv_interpreter::SystemPython::Allowed);
|
||||
find_interpreter(&request, &sources, cache)
|
||||
} else {
|
||||
find_default_python(cache).into_diagnostic()?
|
||||
};
|
||||
find_default_interpreter(cache)
|
||||
}
|
||||
.into_diagnostic()?
|
||||
.into_diagnostic()?
|
||||
.into_interpreter();
|
||||
|
||||
// Add all authenticated sources to the cache.
|
||||
for url in index_locations.urls() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
use assert_cmd::assert::{Assert, OutputAssertExt};
|
||||
use assert_cmd::Command;
|
||||
use assert_fs::assert::PathAssert;
|
||||
|
||||
use assert_fs::fixture::PathChild;
|
||||
use regex::Regex;
|
||||
use std::borrow::BorrowMut;
|
||||
|
|
@ -13,10 +12,13 @@ use std::ffi::OsString;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::Output;
|
||||
use std::str::FromStr;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::Simplified;
|
||||
use uv_interpreter::managed::toolchains_for_version;
|
||||
use uv_interpreter::{find_requested_python, PythonVersion};
|
||||
use uv_interpreter::{
|
||||
find_interpreter, InterpreterRequest, PythonVersion, SourceSelector, VersionRequest,
|
||||
};
|
||||
|
||||
// Exclude any packages uploaded after this date.
|
||||
pub static EXCLUDE_NEWER: &str = "2024-03-25T00:00:00Z";
|
||||
|
|
@ -394,8 +396,14 @@ pub fn python_path_with_versions(
|
|||
.collect::<Vec<_>>();
|
||||
if inner.is_empty() {
|
||||
// Fallback to a system lookup if we failed to find one in the toolchain directory
|
||||
if let Some(interpreter) = find_requested_python(python_version, &cache).unwrap() {
|
||||
vec![interpreter
|
||||
let request = InterpreterRequest::Version(
|
||||
VersionRequest::from_str(python_version)
|
||||
.expect("The test version request must be valid"),
|
||||
);
|
||||
let sources = SourceSelector::All;
|
||||
if let Ok(found) = find_interpreter(&request, &sources, &cache).unwrap() {
|
||||
vec![found
|
||||
.into_interpreter()
|
||||
.sys_executable()
|
||||
.parent()
|
||||
.expect("Python executable should always be in a directory")
|
||||
|
|
@ -409,6 +417,11 @@ pub fn python_path_with_versions(
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(
|
||||
python_versions.is_empty() || !selected_pythons.is_empty(),
|
||||
"Failed to fulfill requested test Python versions: {selected_pythons:?}"
|
||||
);
|
||||
|
||||
Ok(env::join_paths(selected_pythons)?)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ use std::process::Command;
|
|||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use assert_fs::prelude::*;
|
||||
use assert_fs::TempDir;
|
||||
use indoc::indoc;
|
||||
use url::Url;
|
||||
|
||||
|
|
@ -127,29 +126,42 @@ fn missing_requirements_in() {
|
|||
|
||||
#[test]
|
||||
fn missing_venv() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let cache_dir = TempDir::new()?;
|
||||
let venv = temp_dir.child(".venv");
|
||||
let context = TestContext::new("3.12");
|
||||
context.temp_dir.child("requirements.in").touch()?;
|
||||
fs_err::remove_dir_all(context.temp_dir.child(".venv").path())?;
|
||||
|
||||
uv_snapshot!(Command::new(get_bin())
|
||||
.arg("pip")
|
||||
.arg("compile")
|
||||
.arg("requirements.in")
|
||||
.arg("--cache-dir")
|
||||
.arg(cache_dir.path())
|
||||
.env("VIRTUAL_ENV", venv.as_os_str())
|
||||
.current_dir(&temp_dir), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
if cfg!(windows) {
|
||||
uv_snapshot!(context.filters(), context.compile()
|
||||
.arg("requirements.in"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: failed to read from file `requirements.in`
|
||||
Caused by: No such file or directory (os error 2)
|
||||
"###
|
||||
);
|
||||
----- stderr -----
|
||||
warning: Requirements file requirements.in does not contain any dependencies
|
||||
error: failed to canonicalize path `[VENV]/Scripts/python.exe`
|
||||
Caused by: The system cannot find the path specified. (os error 3)
|
||||
"###
|
||||
);
|
||||
} else {
|
||||
uv_snapshot!(context.filters(), context.compile()
|
||||
.arg("requirements.in"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
venv.assert(predicates::path::missing());
|
||||
----- stderr -----
|
||||
warning: Requirements file requirements.in does not contain any dependencies
|
||||
error: failed to canonicalize path `[VENV]/bin/python`
|
||||
Caused by: No such file or directory (os error 2)
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
.child(".venv")
|
||||
.assert(predicates::path::missing());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,14 +102,27 @@ fn missing_venv() -> Result<()> {
|
|||
requirements.write_str("anyio")?;
|
||||
fs::remove_dir_all(&context.venv)?;
|
||||
|
||||
uv_snapshot!(context.filters(), command(&context).arg("requirements.txt"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
if cfg!(windows) {
|
||||
uv_snapshot!(context.filters(), command(&context).arg("requirements.txt"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Virtualenv does not exist at: `[VENV]/`
|
||||
"###);
|
||||
----- stderr -----
|
||||
error: failed to canonicalize path `[VENV]/Scripts/python.exe`
|
||||
Caused by: The system cannot find the path specified. (os error 3)
|
||||
"###);
|
||||
} else {
|
||||
uv_snapshot!(context.filters(), command(&context).arg("requirements.txt"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: failed to canonicalize path `[VENV]/bin/python`
|
||||
Caused by: No such file or directory (os error 2)
|
||||
"###);
|
||||
}
|
||||
|
||||
assert!(predicates::path::missing().eval(&context.venv));
|
||||
|
||||
|
|
|
|||
|
|
@ -220,27 +220,18 @@ fn create_venv_unknown_python_minor() {
|
|||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.15");
|
||||
if cfg!(windows) {
|
||||
uv_snapshot!(&mut command, @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No Python 3.15 found through `py --list-paths` or in `PATH`. Is Python 3.15 installed?
|
||||
"###
|
||||
);
|
||||
} else {
|
||||
uv_snapshot!(&mut command, @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
// Note the `py` launcher is not included in the search in Windows due to
|
||||
// `UV_TEST_PYTHON_PATH` being set
|
||||
uv_snapshot!(&mut command, @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No Python 3.15 in `PATH`. Is Python 3.15 installed?
|
||||
"###
|
||||
);
|
||||
}
|
||||
----- stderr -----
|
||||
× No interpreter found for Python 3.15 in active virtual environment or search path
|
||||
"###
|
||||
);
|
||||
|
||||
context.venv.assert(predicates::path::missing());
|
||||
}
|
||||
|
|
@ -249,17 +240,7 @@ fn create_venv_unknown_python_minor() {
|
|||
fn create_venv_unknown_python_patch() {
|
||||
let context = VenvTestContext::new(&["3.12"]);
|
||||
|
||||
let filters = &[
|
||||
(
|
||||
r"Using Python 3\.\d+\.\d+ interpreter at: .+",
|
||||
"Using Python [VERSION] interpreter at: [PATH]",
|
||||
),
|
||||
(
|
||||
r"No Python 3\.8\.0 found through `py --list-paths` or in `PATH`\. Is Python 3\.8\.0 installed\?",
|
||||
"No Python 3.8.0 in `PATH`. Is Python 3.8.0 installed?",
|
||||
),
|
||||
];
|
||||
uv_snapshot!(filters, context.venv_command()
|
||||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.8.0"), @r###"
|
||||
|
|
@ -268,7 +249,7 @@ fn create_venv_unknown_python_patch() {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No Python 3.8.0 in `PATH`. Is Python 3.8.0 installed?
|
||||
× No interpreter found for Python 3.8.0 in active virtual environment or search path
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue