mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
2693 lines
89 KiB
Rust
2693 lines
89 KiB
Rust
//! Find requested Python interpreters and query interpreters for information.
|
|
use thiserror::Error;
|
|
|
|
#[cfg(test)]
|
|
use uv_static::EnvVars;
|
|
|
|
pub use crate::discovery::{
|
|
EnvironmentPreference, Error as DiscoveryError, PythonDownloads, PythonNotFound,
|
|
PythonPreference, PythonRequest, PythonSource, PythonVariant, VersionRequest,
|
|
find_python_installations,
|
|
};
|
|
pub use crate::downloads::PlatformRequest;
|
|
pub use crate::environment::{InvalidEnvironmentKind, PythonEnvironment};
|
|
pub use crate::implementation::{ImplementationName, LenientImplementationName};
|
|
pub use crate::installation::{
|
|
PythonInstallation, PythonInstallationKey, PythonInstallationMinorVersionKey,
|
|
};
|
|
pub use crate::interpreter::{
|
|
BrokenSymlink, Error as InterpreterError, Interpreter, canonicalize_executable,
|
|
};
|
|
pub use crate::pointer_size::PointerSize;
|
|
pub use crate::prefix::Prefix;
|
|
pub use crate::python_version::PythonVersion;
|
|
pub use crate::target::Target;
|
|
pub use crate::version_files::{
|
|
DiscoveryOptions as VersionFileDiscoveryOptions, FilePreference as VersionFilePreference,
|
|
PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME, PythonVersionFile,
|
|
};
|
|
pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment};
|
|
|
|
mod cpuinfo;
|
|
mod discovery;
|
|
pub mod downloads;
|
|
mod environment;
|
|
mod implementation;
|
|
mod installation;
|
|
mod interpreter;
|
|
mod libc;
|
|
pub mod macos_dylib;
|
|
pub mod managed;
|
|
#[cfg(windows)]
|
|
mod microsoft_store;
|
|
pub mod platform;
|
|
mod pointer_size;
|
|
mod prefix;
|
|
mod python_version;
|
|
mod sysconfig;
|
|
mod target;
|
|
mod version_files;
|
|
mod virtualenv;
|
|
#[cfg(windows)]
|
|
pub mod windows_registry;
|
|
|
|
#[cfg(windows)]
|
|
pub(crate) const COMPANY_KEY: &str = "Astral";
|
|
#[cfg(windows)]
|
|
pub(crate) const COMPANY_DISPLAY_NAME: &str = "Astral Software Inc.";
|
|
|
|
#[cfg(not(test))]
|
|
pub(crate) fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {
|
|
std::env::current_dir()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {
|
|
std::env::var_os(EnvVars::PWD)
|
|
.map(std::path::PathBuf::from)
|
|
.map(Ok)
|
|
.unwrap_or(std::env::current_dir())
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum Error {
|
|
#[error(transparent)]
|
|
Io(#[from] std::io::Error),
|
|
|
|
#[error(transparent)]
|
|
VirtualEnv(#[from] virtualenv::Error),
|
|
|
|
#[error(transparent)]
|
|
Query(#[from] interpreter::Error),
|
|
|
|
#[error(transparent)]
|
|
Discovery(#[from] discovery::Error),
|
|
|
|
#[error(transparent)]
|
|
ManagedPython(#[from] managed::Error),
|
|
|
|
#[error(transparent)]
|
|
Download(#[from] downloads::Error),
|
|
|
|
// TODO(zanieb) We might want to ensure this is always wrapped in another type
|
|
#[error(transparent)]
|
|
KeyError(#[from] installation::PythonInstallationKeyError),
|
|
|
|
#[error(transparent)]
|
|
MissingPython(#[from] PythonNotFound),
|
|
|
|
#[error(transparent)]
|
|
MissingEnvironment(#[from] environment::EnvironmentNotFound),
|
|
|
|
#[error(transparent)]
|
|
InvalidEnvironment(#[from] environment::InvalidEnvironment),
|
|
}
|
|
|
|
// The mock interpreters are not valid on Windows so we don't have unit test coverage there
|
|
// TODO(zanieb): We should write a mock interpreter script that works on Windows
|
|
#[cfg(all(test, unix))]
|
|
mod tests {
|
|
use std::{
|
|
env,
|
|
ffi::{OsStr, OsString},
|
|
path::{Path, PathBuf},
|
|
str::FromStr,
|
|
};
|
|
|
|
use anyhow::Result;
|
|
use assert_fs::{TempDir, fixture::ChildPath, prelude::*};
|
|
use indoc::{formatdoc, indoc};
|
|
use temp_env::with_vars;
|
|
use test_log::test;
|
|
use uv_configuration::PreviewMode;
|
|
use uv_static::EnvVars;
|
|
|
|
use uv_cache::Cache;
|
|
|
|
use crate::{
|
|
PythonNotFound, PythonRequest, PythonSource, PythonVersion,
|
|
implementation::ImplementationName, installation::PythonInstallation,
|
|
managed::ManagedPythonInstallations, virtualenv::virtualenv_python_executable,
|
|
};
|
|
use crate::{
|
|
PythonPreference,
|
|
discovery::{
|
|
self, EnvironmentPreference, find_best_python_installation, find_python_installation,
|
|
},
|
|
};
|
|
|
|
struct TestContext {
|
|
tempdir: TempDir,
|
|
cache: Cache,
|
|
installations: ManagedPythonInstallations,
|
|
search_path: Option<Vec<PathBuf>>,
|
|
workdir: ChildPath,
|
|
}
|
|
|
|
impl TestContext {
|
|
fn new() -> Result<Self> {
|
|
let tempdir = TempDir::new()?;
|
|
let workdir = tempdir.child("workdir");
|
|
workdir.create_dir_all()?;
|
|
|
|
Ok(Self {
|
|
tempdir,
|
|
cache: Cache::temp()?,
|
|
installations: ManagedPythonInstallations::temp()?,
|
|
search_path: None,
|
|
workdir,
|
|
})
|
|
}
|
|
|
|
/// Clear the search path.
|
|
fn reset_search_path(&mut self) {
|
|
self.search_path = None;
|
|
}
|
|
|
|
/// Add a directory to the search path.
|
|
fn add_to_search_path(&mut self, path: PathBuf) {
|
|
match self.search_path.as_mut() {
|
|
Some(paths) => paths.push(path),
|
|
None => self.search_path = Some(vec![path]),
|
|
}
|
|
}
|
|
|
|
/// Create a new directory and add it to the search path.
|
|
fn new_search_path_directory(&mut self, name: impl AsRef<Path>) -> Result<ChildPath> {
|
|
let child = self.tempdir.child(name);
|
|
child.create_dir_all()?;
|
|
self.add_to_search_path(child.to_path_buf());
|
|
Ok(child)
|
|
}
|
|
|
|
fn run<F, R>(&self, closure: F) -> R
|
|
where
|
|
F: FnOnce() -> R,
|
|
{
|
|
self.run_with_vars(&[], closure)
|
|
}
|
|
|
|
fn run_with_vars<F, R>(&self, vars: &[(&str, Option<&OsStr>)], closure: F) -> R
|
|
where
|
|
F: FnOnce() -> R,
|
|
{
|
|
let path = self
|
|
.search_path
|
|
.as_ref()
|
|
.map(|paths| env::join_paths(paths).unwrap());
|
|
|
|
let mut run_vars = vec![
|
|
// Ensure `PATH` is used
|
|
(EnvVars::UV_TEST_PYTHON_PATH, None),
|
|
// Ignore active virtual environments (i.e. that the dev is using)
|
|
(EnvVars::VIRTUAL_ENV, None),
|
|
(EnvVars::PATH, path.as_deref()),
|
|
// Use the temporary python directory
|
|
(
|
|
EnvVars::UV_PYTHON_INSTALL_DIR,
|
|
Some(self.installations.root().as_os_str()),
|
|
),
|
|
// Set a working directory
|
|
("PWD", Some(self.workdir.path().as_os_str())),
|
|
];
|
|
for (key, value) in vars {
|
|
run_vars.push((key, *value));
|
|
}
|
|
with_vars(&run_vars, closure)
|
|
}
|
|
|
|
/// Create a fake Python interpreter executable which returns fixed metadata mocking our interpreter
|
|
/// query script output.
|
|
fn create_mock_interpreter(
|
|
path: &Path,
|
|
version: &PythonVersion,
|
|
implementation: ImplementationName,
|
|
system: bool,
|
|
free_threaded: bool,
|
|
) -> Result<()> {
|
|
let json = indoc! {r##"
|
|
{
|
|
"result": "success",
|
|
"platform": {
|
|
"os": {
|
|
"name": "manylinux",
|
|
"major": 2,
|
|
"minor": 38
|
|
},
|
|
"arch": "x86_64"
|
|
},
|
|
"manylinux_compatible": true,
|
|
"standalone": true,
|
|
"markers": {
|
|
"implementation_name": "{IMPLEMENTATION}",
|
|
"implementation_version": "{FULL_VERSION}",
|
|
"os_name": "posix",
|
|
"platform_machine": "x86_64",
|
|
"platform_python_implementation": "{IMPLEMENTATION}",
|
|
"platform_release": "6.5.0-13-generic",
|
|
"platform_system": "Linux",
|
|
"platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023",
|
|
"python_full_version": "{FULL_VERSION}",
|
|
"python_version": "{VERSION}",
|
|
"sys_platform": "linux"
|
|
},
|
|
"sys_base_exec_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
|
|
"sys_base_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
|
|
"sys_prefix": "{PREFIX}",
|
|
"sys_executable": "{PATH}",
|
|
"sys_path": [
|
|
"/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/lib/python{VERSION}",
|
|
"/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages"
|
|
],
|
|
"stdlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}",
|
|
"scheme": {
|
|
"data": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
|
|
"include": "/home/ferris/.pyenv/versions/{FULL_VERSION}/include",
|
|
"platlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages",
|
|
"purelib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages",
|
|
"scripts": "/home/ferris/.pyenv/versions/{FULL_VERSION}/bin"
|
|
},
|
|
"virtualenv": {
|
|
"data": "",
|
|
"include": "include",
|
|
"platlib": "lib/python{VERSION}/site-packages",
|
|
"purelib": "lib/python{VERSION}/site-packages",
|
|
"scripts": "bin"
|
|
},
|
|
"pointer_size": "64",
|
|
"gil_disabled": {FREE_THREADED}
|
|
}
|
|
"##};
|
|
|
|
let json = if system {
|
|
json.replace("{PREFIX}", "/home/ferris/.pyenv/versions/{FULL_VERSION}")
|
|
} else {
|
|
json.replace("{PREFIX}", "/home/ferris/projects/uv/.venv")
|
|
};
|
|
|
|
let json = json
|
|
.replace(
|
|
"{PATH}",
|
|
path.to_str().expect("Path can be represented as string"),
|
|
)
|
|
.replace("{FULL_VERSION}", &version.to_string())
|
|
.replace("{VERSION}", &version.without_patch().to_string())
|
|
.replace("{FREE_THREADED}", &free_threaded.to_string())
|
|
.replace("{IMPLEMENTATION}", (&implementation).into());
|
|
|
|
fs_err::create_dir_all(path.parent().unwrap())?;
|
|
fs_err::write(
|
|
path,
|
|
formatdoc! {r"
|
|
#!/bin/sh
|
|
echo '{json}'
|
|
"},
|
|
)?;
|
|
|
|
fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Create a mock Python 2 interpreter executable which returns a fixed error message mocking
|
|
/// invocation of Python 2 with the `-I` flag as done by our query script.
|
|
fn create_mock_python2_interpreter(path: &Path) -> Result<()> {
|
|
let output = indoc! { r"
|
|
Unknown option: -I
|
|
usage: /usr/bin/python [option] ... [-c cmd | -m mod | file | -] [arg] ...
|
|
Try `python -h` for more information.
|
|
"};
|
|
|
|
fs_err::write(
|
|
path,
|
|
formatdoc! {r"
|
|
#!/bin/sh
|
|
echo '{output}' 1>&2
|
|
"},
|
|
)?;
|
|
|
|
fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Create child directories in a temporary directory.
|
|
fn new_search_path_directories(
|
|
&mut self,
|
|
names: &[impl AsRef<Path>],
|
|
) -> Result<Vec<ChildPath>> {
|
|
let paths = names
|
|
.iter()
|
|
.map(|name| self.new_search_path_directory(name))
|
|
.collect::<Result<Vec<_>>>()?;
|
|
Ok(paths)
|
|
}
|
|
|
|
/// Create fake Python interpreters the given Python versions.
|
|
///
|
|
/// Adds them to the test context search path.
|
|
fn add_python_to_workdir(&self, name: &str, version: &str) -> Result<()> {
|
|
TestContext::create_mock_interpreter(
|
|
self.workdir.child(name).as_ref(),
|
|
&PythonVersion::from_str(version).expect("Test uses valid version"),
|
|
ImplementationName::default(),
|
|
true,
|
|
false,
|
|
)
|
|
}
|
|
|
|
/// Create fake Python interpreters the given Python versions.
|
|
///
|
|
/// Adds them to the test context search path.
|
|
fn add_python_versions(&mut self, versions: &[&'static str]) -> Result<()> {
|
|
let interpreters: Vec<_> = versions
|
|
.iter()
|
|
.map(|version| (true, ImplementationName::default(), "python", *version))
|
|
.collect();
|
|
self.add_python_interpreters(interpreters.as_slice())
|
|
}
|
|
|
|
/// Create fake Python interpreters the given Python implementations and versions.
|
|
///
|
|
/// Adds them to the test context search path.
|
|
fn add_python_interpreters(
|
|
&mut self,
|
|
kinds: &[(bool, ImplementationName, &'static str, &'static str)],
|
|
) -> Result<()> {
|
|
// Generate a "unique" folder name for each interpreter
|
|
let names: Vec<OsString> = kinds
|
|
.iter()
|
|
.map(|(system, implementation, name, version)| {
|
|
OsString::from_str(&format!("{system}-{implementation}-{name}-{version}"))
|
|
.unwrap()
|
|
})
|
|
.collect();
|
|
let paths = self.new_search_path_directories(names.as_slice())?;
|
|
for (path, (system, implementation, executable, version)) in
|
|
itertools::zip_eq(&paths, kinds)
|
|
{
|
|
let python = format!("{executable}{}", env::consts::EXE_SUFFIX);
|
|
Self::create_mock_interpreter(
|
|
&path.join(python),
|
|
&PythonVersion::from_str(version).unwrap(),
|
|
*implementation,
|
|
*system,
|
|
false,
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Create a mock virtual environment at the given directory
|
|
fn mock_venv(path: impl AsRef<Path>, version: &'static str) -> Result<()> {
|
|
let executable = virtualenv_python_executable(path.as_ref());
|
|
fs_err::create_dir_all(
|
|
executable
|
|
.parent()
|
|
.expect("A Python executable path should always have a parent"),
|
|
)?;
|
|
TestContext::create_mock_interpreter(
|
|
&executable,
|
|
&PythonVersion::from_str(version)
|
|
.expect("A valid Python version is used for tests"),
|
|
ImplementationName::default(),
|
|
false,
|
|
false,
|
|
)?;
|
|
ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Create a mock conda prefix at the given directory.
|
|
///
|
|
/// These are like virtual environments but they look like system interpreters because `prefix` and `base_prefix` are equal.
|
|
fn mock_conda_prefix(path: impl AsRef<Path>, version: &'static str) -> Result<()> {
|
|
let executable = virtualenv_python_executable(&path);
|
|
fs_err::create_dir_all(
|
|
executable
|
|
.parent()
|
|
.expect("A Python executable path should always have a parent"),
|
|
)?;
|
|
TestContext::create_mock_interpreter(
|
|
&executable,
|
|
&PythonVersion::from_str(version)
|
|
.expect("A valid Python version is used for tests"),
|
|
ImplementationName::default(),
|
|
true,
|
|
false,
|
|
)?;
|
|
ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_empty_path() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
|
|
context.search_path = Some(vec![]);
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::default(),
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
});
|
|
assert!(
|
|
matches!(result, Ok(Err(PythonNotFound { .. }))),
|
|
"With an empty path, no Python installation should be detected got {result:?}"
|
|
);
|
|
|
|
context.search_path = None;
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::default(),
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
});
|
|
assert!(
|
|
matches!(result, Ok(Err(PythonNotFound { .. }))),
|
|
"With an unset path, no Python installation should be detected got {result:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_unexecutable_file() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context
|
|
.new_search_path_directory("path")?
|
|
.child(format!("python{}", env::consts::EXE_SUFFIX))
|
|
.touch()?;
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::default(),
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
});
|
|
assert!(
|
|
matches!(result, Ok(Err(PythonNotFound { .. }))),
|
|
"With an non-executable Python, no Python installation should be detected; got {result:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_valid_executable() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.12.1"])?;
|
|
|
|
let interpreter = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::default(),
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert!(
|
|
matches!(
|
|
interpreter,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPathFirst,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should find the valid executable; got {interpreter:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_valid_executable_after_invalid() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
let children = context.new_search_path_directories(&[
|
|
"query-parse-error",
|
|
"not-executable",
|
|
"empty",
|
|
"good",
|
|
])?;
|
|
|
|
// An executable file with a bad response
|
|
#[cfg(unix)]
|
|
fs_err::write(
|
|
children[0].join(format!("python{}", env::consts::EXE_SUFFIX)),
|
|
formatdoc! {r"
|
|
#!/bin/sh
|
|
echo 'foo'
|
|
"},
|
|
)?;
|
|
fs_err::set_permissions(
|
|
children[0].join(format!("python{}", env::consts::EXE_SUFFIX)),
|
|
std::os::unix::fs::PermissionsExt::from_mode(0o770),
|
|
)?;
|
|
|
|
// A non-executable file
|
|
ChildPath::new(children[1].join(format!("python{}", env::consts::EXE_SUFFIX))).touch()?;
|
|
|
|
// An empty directory at `children[2]`
|
|
|
|
// An good interpreter!
|
|
let python_path = children[3].join(format!("python{}", env::consts::EXE_SUFFIX));
|
|
TestContext::create_mock_interpreter(
|
|
&python_path,
|
|
&PythonVersion::from_str("3.12.1").unwrap(),
|
|
ImplementationName::default(),
|
|
true,
|
|
false,
|
|
)?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::default(),
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPath,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should skip the bad executables in favor of the good one; got {python:?}"
|
|
);
|
|
assert_eq!(python.interpreter().sys_executable(), python_path);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_only_python2_executable() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
let python = context
|
|
.new_search_path_directory("python2")?
|
|
.child(format!("python{}", env::consts::EXE_SUFFIX));
|
|
TestContext::create_mock_python2_interpreter(&python)?;
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::default(),
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
});
|
|
assert!(
|
|
matches!(result, Err(discovery::Error::Query(..))),
|
|
"If only Python 2 is available, we should report the interpreter query error; got {result:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_skip_python2_executable() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
|
|
let python2 = context
|
|
.new_search_path_directory("python2")?
|
|
.child(format!("python{}", env::consts::EXE_SUFFIX));
|
|
TestContext::create_mock_python2_interpreter(&python2)?;
|
|
|
|
let python3 = context
|
|
.new_search_path_directory("python3")?
|
|
.child(format!("python{}", env::consts::EXE_SUFFIX));
|
|
TestContext::create_mock_interpreter(
|
|
&python3,
|
|
&PythonVersion::from_str("3.12.1").unwrap(),
|
|
ImplementationName::default(),
|
|
true,
|
|
false,
|
|
)?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::default(),
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPath,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should skip the Python 2 installation and find the Python 3 interpreter; got {python:?}"
|
|
);
|
|
assert_eq!(python.interpreter().sys_executable(), python3.path());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_system_python_allowed() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(false, ImplementationName::CPython, "python", "3.10.0"),
|
|
(true, ImplementationName::CPython, "python", "3.10.1"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"Should find the first interpreter regardless of system"
|
|
);
|
|
|
|
// Reverse the order of the virtual environment and system
|
|
context.reset_search_path();
|
|
context.add_python_interpreters(&[
|
|
(true, ImplementationName::CPython, "python", "3.10.1"),
|
|
(false, ImplementationName::CPython, "python", "3.10.0"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"Should find the first interpreter regardless of system"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_system_python_required() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(false, ImplementationName::CPython, "python", "3.10.0"),
|
|
(true, ImplementationName::CPython, "python", "3.10.1"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"Should skip the virtual environment"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_system_python_disallowed() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(true, ImplementationName::CPython, "python", "3.10.0"),
|
|
(false, ImplementationName::CPython, "python", "3.10.1"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"Should skip the system Python"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_version_minor() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.11"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPath,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should find a python; got {python:?}"
|
|
);
|
|
assert_eq!(
|
|
&python.interpreter().python_full_version().to_string(),
|
|
"3.11.2",
|
|
"We should find the correct interpreter for the request"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_version_patch() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.10.1", "3.11.3", "3.11.2", "3.12.3"])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.11.2"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPath,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should find a python; got {python:?}"
|
|
);
|
|
assert_eq!(
|
|
&python.interpreter().python_full_version().to_string(),
|
|
"3.11.2",
|
|
"We should find the correct interpreter for the request"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_version_minor_no_match() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?;
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.9"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find a python; got {result:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_version_patch_no_match() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?;
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.11.9"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find a python; got {result:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_best_python_version_patch_exact() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?;
|
|
|
|
let python = context.run(|| {
|
|
find_best_python_installation(
|
|
&PythonRequest::parse("3.11.3"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPath,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should find a python; got {python:?}"
|
|
);
|
|
assert_eq!(
|
|
&python.interpreter().python_full_version().to_string(),
|
|
"3.11.3",
|
|
"We should prefer the exact request"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_best_python_version_patch_fallback() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?;
|
|
|
|
let python = context.run(|| {
|
|
find_best_python_installation(
|
|
&PythonRequest::parse("3.11.11"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPath,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should find a python; got {python:?}"
|
|
);
|
|
assert_eq!(
|
|
&python.interpreter().python_full_version().to_string(),
|
|
"3.11.2",
|
|
"We should fallback to the first matching minor"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_best_python_skips_source_without_match() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
let venv = context.tempdir.child(".venv");
|
|
TestContext::mock_venv(&venv, "3.12.0")?;
|
|
context.add_python_versions(&["3.10.1"])?;
|
|
|
|
let python =
|
|
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
|
|
find_best_python_installation(
|
|
&PythonRequest::parse("3.10"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPathFirst,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should skip the active environment in favor of the requested version; got {python:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_best_python_returns_to_earlier_source_on_fallback() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
let venv = context.tempdir.child(".venv");
|
|
TestContext::mock_venv(&venv, "3.10.1")?;
|
|
context.add_python_versions(&["3.10.3"])?;
|
|
|
|
let python =
|
|
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
|
|
find_best_python_installation(
|
|
&PythonRequest::parse("3.10.2"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::ActiveEnvironment,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should prefer the active environment after relaxing; got {python:?}"
|
|
);
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should prefer the active environment"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_from_active_python() -> Result<()> {
|
|
let context = TestContext::new()?;
|
|
let venv = context.tempdir.child("some-venv");
|
|
TestContext::mock_venv(&venv, "3.12.0")?;
|
|
|
|
let python =
|
|
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should prefer the active environment"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_from_active_python_prerelease() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.12.0"])?;
|
|
let venv = context.tempdir.child("some-venv");
|
|
TestContext::mock_venv(&venv, "3.13.0rc1")?;
|
|
|
|
let python =
|
|
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.13.0rc1",
|
|
"We should prefer the active environment"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_latest() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.8.10", "3.11.5", "3.9.18", "3.10.12"])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Latest,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
)
|
|
})??;
|
|
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPath,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should find a python; got {python:?}"
|
|
);
|
|
assert_eq!(
|
|
&python.interpreter().python_full_version().to_string(),
|
|
"3.11.5",
|
|
"We should find the latest version (3.11.5)"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_latest_with_prereleases() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.10.1", "3.11.2", "3.12.0rc1", "3.13.0a1"])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Latest,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
)
|
|
})??;
|
|
|
|
assert_eq!(
|
|
&python.interpreter().python_full_version().to_string(),
|
|
"3.11.2",
|
|
"Latest should find the highest stable version"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_latest_no_pythons() -> Result<()> {
|
|
let context = TestContext::new()?;
|
|
// Don't add any Python versions
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Latest,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
)
|
|
})?;
|
|
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find any python when none are available; got {result:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_latest_only_prereleases() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.12.0rc1", "3.13.0a1", "3.11.0b2"])?;
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Latest,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
)
|
|
})?;
|
|
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find any python when only prereleases are available; got {result:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_from_conda_prefix() -> Result<()> {
|
|
let context = TestContext::new()?;
|
|
let condaenv = context.tempdir.child("condaenv");
|
|
TestContext::mock_conda_prefix(&condaenv, "3.12.0")?;
|
|
|
|
let python = context.run_with_vars(
|
|
&[(EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str()))],
|
|
|| {
|
|
// Note this python is not treated as a system interpreter
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlyVirtual,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should allow the active conda python"
|
|
);
|
|
|
|
let baseenv = context.tempdir.child("base");
|
|
TestContext::mock_conda_prefix(&baseenv, "3.12.1")?;
|
|
|
|
// But not if it's a base environment
|
|
let result = context.run_with_vars(
|
|
&[
|
|
("CONDA_PREFIX", Some(baseenv.as_os_str())),
|
|
("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))),
|
|
],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlyVirtual,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)?;
|
|
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not allow the non-virtual environment; got {result:?}"
|
|
);
|
|
|
|
// Unless, system interpreters are included...
|
|
let python = context.run_with_vars(
|
|
&[
|
|
("CONDA_PREFIX", Some(baseenv.as_os_str())),
|
|
("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))),
|
|
],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.1",
|
|
"We should find the base conda environment"
|
|
);
|
|
|
|
// If the environment name doesn't match the default, we should not treat it as system
|
|
let python = context.run_with_vars(
|
|
&[
|
|
("CONDA_PREFIX", Some(condaenv.as_os_str())),
|
|
("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))),
|
|
],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlyVirtual,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should find the conda environment"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_from_conda_prefix_and_virtualenv() -> Result<()> {
|
|
let context = TestContext::new()?;
|
|
let venv = context.tempdir.child(".venv");
|
|
TestContext::mock_venv(&venv, "3.12.0")?;
|
|
let condaenv = context.tempdir.child("condaenv");
|
|
TestContext::mock_conda_prefix(&condaenv, "3.12.1")?;
|
|
|
|
let python = context.run_with_vars(
|
|
&[
|
|
(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
|
|
(EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())),
|
|
],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should prefer the non-conda python"
|
|
);
|
|
|
|
// Put a virtual environment in the working directory
|
|
let venv = context.workdir.child(".venv");
|
|
TestContext::mock_venv(venv, "3.12.2")?;
|
|
let python = context.run_with_vars(
|
|
&[(EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str()))],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.1",
|
|
"We should prefer the conda python over inactive virtual environments"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_from_discovered_python() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
|
|
// Create a virtual environment in a parent of the workdir
|
|
let venv = context.tempdir.child(".venv");
|
|
TestContext::mock_venv(venv, "3.12.0")?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should find the python"
|
|
);
|
|
|
|
// Add some system versions to ensure we don't use those
|
|
context.add_python_versions(&["3.12.1", "3.12.2"])?;
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should prefer the discovered virtual environment over available system versions"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_skips_broken_active_python() -> Result<()> {
|
|
let context = TestContext::new()?;
|
|
let venv = context.tempdir.child(".venv");
|
|
TestContext::mock_venv(&venv, "3.12.0")?;
|
|
|
|
// Delete the pyvenv cfg to break the virtualenv
|
|
fs_err::remove_file(venv.join("pyvenv.cfg"))?;
|
|
|
|
let python =
|
|
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
// TODO(zanieb): We should skip this python, why don't we?
|
|
"We should prefer the active environment"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_from_parent_interpreter() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
|
|
let parent = context.tempdir.child("python").to_path_buf();
|
|
TestContext::create_mock_interpreter(
|
|
&parent,
|
|
&PythonVersion::from_str("3.12.0").unwrap(),
|
|
ImplementationName::CPython,
|
|
// Note we mark this as a system interpreter instead of a virtual environment
|
|
true,
|
|
false,
|
|
)?;
|
|
|
|
let python = context.run_with_vars(
|
|
&[(
|
|
EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
|
|
Some(parent.as_os_str()),
|
|
)],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should find the parent interpreter"
|
|
);
|
|
|
|
// Parent interpreters are preferred over virtual environments and system interpreters
|
|
let venv = context.tempdir.child(".venv");
|
|
TestContext::mock_venv(&venv, "3.12.2")?;
|
|
context.add_python_versions(&["3.12.3"])?;
|
|
let python = context.run_with_vars(
|
|
&[
|
|
(
|
|
EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
|
|
Some(parent.as_os_str()),
|
|
),
|
|
(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
|
|
],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should prefer the parent interpreter"
|
|
);
|
|
|
|
// Test with `EnvironmentPreference::ExplicitSystem`
|
|
let python = context.run_with_vars(
|
|
&[
|
|
(
|
|
EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
|
|
Some(parent.as_os_str()),
|
|
),
|
|
(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
|
|
],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::ExplicitSystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should prefer the parent interpreter"
|
|
);
|
|
|
|
// Test with `EnvironmentPreference::OnlySystem`
|
|
let python = context.run_with_vars(
|
|
&[
|
|
(
|
|
EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
|
|
Some(parent.as_os_str()),
|
|
),
|
|
(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
|
|
],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should prefer the parent interpreter since it's not virtual"
|
|
);
|
|
|
|
// Test with `EnvironmentPreference::OnlyVirtual`
|
|
let python = context.run_with_vars(
|
|
&[
|
|
(
|
|
EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
|
|
Some(parent.as_os_str()),
|
|
),
|
|
(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())),
|
|
],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlyVirtual,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.2",
|
|
"We find the virtual environment Python because a system is explicitly not allowed"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_from_parent_interpreter_prerelease() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.12.0"])?;
|
|
let parent = context.tempdir.child("python").to_path_buf();
|
|
TestContext::create_mock_interpreter(
|
|
&parent,
|
|
&PythonVersion::from_str("3.13.0rc2").unwrap(),
|
|
ImplementationName::CPython,
|
|
// Note we mark this as a system interpreter instead of a virtual environment
|
|
true,
|
|
false,
|
|
)?;
|
|
|
|
let python = context.run_with_vars(
|
|
&[(
|
|
EnvVars::UV_INTERNAL__PARENT_INTERPRETER,
|
|
Some(parent.as_os_str()),
|
|
)],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.13.0rc2",
|
|
"We should find the parent interpreter"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_active_python_skipped_if_system_required() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
let venv = context.tempdir.child(".venv");
|
|
TestContext::mock_venv(&venv, "3.9.0")?;
|
|
context.add_python_versions(&["3.10.0", "3.11.1", "3.12.2"])?;
|
|
|
|
// Without a specific request
|
|
let python =
|
|
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should skip the active environment"
|
|
);
|
|
|
|
// With a requested minor version
|
|
let python =
|
|
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.12"),
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.2",
|
|
"We should skip the active environment"
|
|
);
|
|
|
|
// With a patch version that cannot be python
|
|
let result =
|
|
context.run_with_vars(&[(EnvVars::VIRTUAL_ENV, Some(venv.as_os_str()))], || {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.12.3"),
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
result.is_err(),
|
|
"We should not find an python; got {result:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_fails_if_no_virtualenv_and_system_not_allowed() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_versions(&["3.10.1", "3.11.2"])?;
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::OnlyVirtual,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find an python; got {result:?}"
|
|
);
|
|
|
|
// With an invalid virtual environment variable
|
|
let result = context.run_with_vars(
|
|
&[(EnvVars::VIRTUAL_ENV, Some(context.tempdir.as_os_str()))],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.12.3"),
|
|
EnvironmentPreference::OnlySystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find an python; got {result:?}"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_allows_name_in_working_directory() -> Result<()> {
|
|
let context = TestContext::new()?;
|
|
context.add_python_to_workdir("foobar", "3.10.0")?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("foobar"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should find the named executable"
|
|
);
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find it without a specific request"
|
|
);
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.10.0"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find it via a matching version request"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_allows_relative_file_path() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
let python = context.workdir.child("foo").join("bar");
|
|
TestContext::create_mock_interpreter(
|
|
&python,
|
|
&PythonVersion::from_str("3.10.0").unwrap(),
|
|
ImplementationName::default(),
|
|
true,
|
|
false,
|
|
)?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("./foo/bar"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should find the `bar` executable"
|
|
);
|
|
|
|
context.add_python_versions(&["3.11.1"])?;
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("./foo/bar"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should prefer the `bar` executable over the system and virtualenvs"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_allows_absolute_file_path() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
let python_path = context.tempdir.child("foo").join("bar");
|
|
TestContext::create_mock_interpreter(
|
|
&python_path,
|
|
&PythonVersion::from_str("3.10.0").unwrap(),
|
|
ImplementationName::default(),
|
|
true,
|
|
false,
|
|
)?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(python_path.to_str().unwrap()),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should find the `bar` executable"
|
|
);
|
|
|
|
// With `EnvironmentPreference::ExplicitSystem`
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(python_path.to_str().unwrap()),
|
|
EnvironmentPreference::ExplicitSystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should allow the `bar` executable with explicit system"
|
|
);
|
|
|
|
// With `EnvironmentPreference::OnlyVirtual`
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(python_path.to_str().unwrap()),
|
|
EnvironmentPreference::OnlyVirtual,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should allow the `bar` executable and verify it is virtual"
|
|
);
|
|
|
|
context.add_python_versions(&["3.11.1"])?;
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(python_path.to_str().unwrap()),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should prefer the `bar` executable over the system and virtualenvs"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_allows_venv_directory_path() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
|
|
let venv = context.tempdir.child("foo").child(".venv");
|
|
TestContext::mock_venv(&venv, "3.10.0")?;
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("../foo/.venv"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should find the relative venv path"
|
|
);
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(venv.to_str().unwrap()),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should find the absolute venv path"
|
|
);
|
|
|
|
// We should allow it to be a directory that _looks_ like a virtual environment.
|
|
let python_path = context.tempdir.child("bar").join("bin").join("python");
|
|
TestContext::create_mock_interpreter(
|
|
&python_path,
|
|
&PythonVersion::from_str("3.10.0").unwrap(),
|
|
ImplementationName::default(),
|
|
true,
|
|
false,
|
|
)?;
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(context.tempdir.child("bar").to_str().unwrap()),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should find the executable in the directory"
|
|
);
|
|
|
|
let other_venv = context.tempdir.child("foobar").child(".venv");
|
|
TestContext::mock_venv(&other_venv, "3.11.1")?;
|
|
context.add_python_versions(&["3.12.2"])?;
|
|
let python = context.run_with_vars(
|
|
&[(EnvVars::VIRTUAL_ENV, Some(other_venv.as_os_str()))],
|
|
|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(venv.to_str().unwrap()),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
},
|
|
)??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should prefer the requested directory over the system and active virtual environments"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_venv_symlink() -> Result<()> {
|
|
let context = TestContext::new()?;
|
|
|
|
let venv = context.tempdir.child("target").child("env");
|
|
TestContext::mock_venv(&venv, "3.10.6")?;
|
|
let symlink = context.tempdir.child("proj").child(".venv");
|
|
context.tempdir.child("proj").create_dir_all()?;
|
|
symlink.symlink_to_dir(venv)?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("../proj/.venv"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.6",
|
|
"We should find the symlinked venv"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_treats_missing_file_path_as_file() -> Result<()> {
|
|
let context = TestContext::new()?;
|
|
context.workdir.child("foo").create_dir_all()?;
|
|
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("./foo/bar"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find the file; got {result:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_executable_name_in_search_path() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
let python = context.tempdir.child("foo").join("bar");
|
|
TestContext::create_mock_interpreter(
|
|
&python,
|
|
&PythonVersion::from_str("3.10.0").unwrap(),
|
|
ImplementationName::default(),
|
|
true,
|
|
false,
|
|
)?;
|
|
context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("bar"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should find the `bar` executable"
|
|
);
|
|
|
|
// With [`EnvironmentPreference::OnlyVirtual`], we should not allow the interpreter
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("bar"),
|
|
EnvironmentPreference::ExplicitSystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not allow a system interpreter; got {result:?}"
|
|
);
|
|
|
|
// Unless it's a virtual environment interpreter
|
|
let mut context = TestContext::new()?;
|
|
let python = context.tempdir.child("foo").join("bar");
|
|
TestContext::create_mock_interpreter(
|
|
&python,
|
|
&PythonVersion::from_str("3.10.0").unwrap(),
|
|
ImplementationName::default(),
|
|
false, // Not a system interpreter
|
|
false,
|
|
)?;
|
|
context.add_to_search_path(context.tempdir.child("foo").to_path_buf());
|
|
|
|
let python = context
|
|
.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("bar"),
|
|
EnvironmentPreference::ExplicitSystem,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should find the `bar` executable"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_pypy() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
|
|
context.add_python_interpreters(&[(true, ImplementationName::PyPy, "pypy", "3.10.0")])?;
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not find the pypy interpreter if not named `python` or requested; got {result:?}"
|
|
);
|
|
|
|
// But we should find it
|
|
context.reset_search_path();
|
|
context.add_python_interpreters(&[(true, ImplementationName::PyPy, "python", "3.10.1")])?;
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should find the pypy interpreter if it's the only one"
|
|
);
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("pypy"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should find the pypy interpreter if it's requested"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_pypy_request_ignores_cpython() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(true, ImplementationName::CPython, "python", "3.10.0"),
|
|
(true, ImplementationName::PyPy, "pypy", "3.10.1"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("pypy"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should skip the CPython interpreter"
|
|
);
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should take the first interpreter without a specific request"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_pypy_request_skips_wrong_versions() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(true, ImplementationName::PyPy, "pypy", "3.9"),
|
|
(true, ImplementationName::PyPy, "pypy", "3.10.1"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("pypy3.10"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should skip the first interpreter"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_pypy_finds_executable_with_version_name() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(true, ImplementationName::PyPy, "pypy3.9", "3.10.0"), // We don't consider this one because of the executable name
|
|
(true, ImplementationName::PyPy, "pypy3.10", "3.10.1"),
|
|
(true, ImplementationName::PyPy, "pypy", "3.10.2"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("pypy@3.10"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should find the requested interpreter version"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_all_minors() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(true, ImplementationName::CPython, "python", "3.10.0"),
|
|
(true, ImplementationName::CPython, "python3", "3.10.0"),
|
|
(true, ImplementationName::CPython, "python3.12", "3.12.0"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(">= 3.11"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0",
|
|
"We should find matching minor version even if they aren't called `python` or `python3`"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_all_minors_prerelease() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(true, ImplementationName::CPython, "python", "3.10.0"),
|
|
(true, ImplementationName::CPython, "python3", "3.10.0"),
|
|
(true, ImplementationName::CPython, "python3.11", "3.11.0b0"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(">= 3.11"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.11.0b0",
|
|
"We should find the 3.11 prerelease even though >=3.11 would normally exclude prereleases"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_all_minors_prerelease_next() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(true, ImplementationName::CPython, "python", "3.10.0"),
|
|
(true, ImplementationName::CPython, "python3", "3.10.0"),
|
|
(true, ImplementationName::CPython, "python3.12", "3.12.0b0"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse(">= 3.11"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.12.0b0",
|
|
"We should find the 3.12 prerelease"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_graalpy() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
|
|
context.add_python_interpreters(&[(
|
|
true,
|
|
ImplementationName::GraalPy,
|
|
"graalpy",
|
|
"3.10.0",
|
|
)])?;
|
|
let result = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})?;
|
|
assert!(
|
|
matches!(result, Err(PythonNotFound { .. })),
|
|
"We should not the graalpy interpreter if not named `python` or requested; got {result:?}"
|
|
);
|
|
|
|
// But we should find it
|
|
context.reset_search_path();
|
|
context.add_python_interpreters(&[(
|
|
true,
|
|
ImplementationName::GraalPy,
|
|
"python",
|
|
"3.10.1",
|
|
)])?;
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should find the graalpy interpreter if it's the only one"
|
|
);
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("graalpy"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should find the graalpy interpreter if it's requested"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_graalpy_request_ignores_cpython() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
context.add_python_interpreters(&[
|
|
(true, ImplementationName::CPython, "python", "3.10.0"),
|
|
(true, ImplementationName::GraalPy, "graalpy", "3.10.1"),
|
|
])?;
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("graalpy"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should skip the CPython interpreter"
|
|
);
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::Default,
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should take the first interpreter without a specific request"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_executable_name_preference() -> 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,
|
|
false,
|
|
)?;
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("pypy"),
|
|
&PythonVersion::from_str("3.10.1").unwrap(),
|
|
ImplementationName::PyPy,
|
|
true,
|
|
false,
|
|
)?;
|
|
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,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.0",
|
|
"We should prefer the versioned one when a version is requested"
|
|
);
|
|
|
|
let python = context
|
|
.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("pypy"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should prefer the generic one when no version is requested"
|
|
);
|
|
|
|
let mut context = TestContext::new()?;
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("python3.10"),
|
|
&PythonVersion::from_str("3.10.0").unwrap(),
|
|
ImplementationName::PyPy,
|
|
true,
|
|
false,
|
|
)?;
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("pypy"),
|
|
&PythonVersion::from_str("3.10.1").unwrap(),
|
|
ImplementationName::PyPy,
|
|
true,
|
|
false,
|
|
)?;
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("python"),
|
|
&PythonVersion::from_str("3.10.2").unwrap(),
|
|
ImplementationName::PyPy,
|
|
true,
|
|
false,
|
|
)?;
|
|
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,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
"We should prefer the implementation name over the generic name"
|
|
);
|
|
|
|
let python = context
|
|
.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("default"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.2",
|
|
"We should prefer the generic name over the implementation name, but not the versioned name"
|
|
);
|
|
|
|
// We prefer `python` executables over `graalpy` executables in the same directory
|
|
// if they are both GraalPy
|
|
let mut context = TestContext::new()?;
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("python"),
|
|
&PythonVersion::from_str("3.10.0").unwrap(),
|
|
ImplementationName::GraalPy,
|
|
true,
|
|
false,
|
|
)?;
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("graalpy"),
|
|
&PythonVersion::from_str("3.10.1").unwrap(),
|
|
ImplementationName::GraalPy,
|
|
true,
|
|
false,
|
|
)?;
|
|
context.add_to_search_path(context.tempdir.to_path_buf());
|
|
|
|
let python = context
|
|
.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("graalpy@3.10"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.1",
|
|
);
|
|
|
|
// 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"),
|
|
(true, ImplementationName::GraalPy, "graalpy", "3.10.3"),
|
|
])?;
|
|
let python = context
|
|
.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("graalpy@3.10"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.2",
|
|
);
|
|
|
|
// And `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,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
python.interpreter().python_full_version().to_string(),
|
|
"3.10.3",
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_version_free_threaded() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("python"),
|
|
&PythonVersion::from_str("3.13.1").unwrap(),
|
|
ImplementationName::CPython,
|
|
true,
|
|
false,
|
|
)?;
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("python3.13t"),
|
|
&PythonVersion::from_str("3.13.0").unwrap(),
|
|
ImplementationName::CPython,
|
|
true,
|
|
true,
|
|
)?;
|
|
context.add_to_search_path(context.tempdir.to_path_buf());
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.13t"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPathFirst,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should find a python; got {python:?}"
|
|
);
|
|
assert_eq!(
|
|
&python.interpreter().python_full_version().to_string(),
|
|
"3.13.0",
|
|
"We should find the correct interpreter for the request"
|
|
);
|
|
assert!(
|
|
&python.interpreter().gil_disabled(),
|
|
"We should find a python without the GIL"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn find_python_version_prefer_non_free_threaded() -> Result<()> {
|
|
let mut context = TestContext::new()?;
|
|
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("python"),
|
|
&PythonVersion::from_str("3.13.0").unwrap(),
|
|
ImplementationName::CPython,
|
|
true,
|
|
false,
|
|
)?;
|
|
TestContext::create_mock_interpreter(
|
|
&context.tempdir.join("python3.13t"),
|
|
&PythonVersion::from_str("3.13.0").unwrap(),
|
|
ImplementationName::CPython,
|
|
true,
|
|
true,
|
|
)?;
|
|
context.add_to_search_path(context.tempdir.to_path_buf());
|
|
|
|
let python = context.run(|| {
|
|
find_python_installation(
|
|
&PythonRequest::parse("3.13"),
|
|
EnvironmentPreference::Any,
|
|
PythonPreference::OnlySystem,
|
|
&context.cache,
|
|
PreviewMode::Disabled,
|
|
)
|
|
})??;
|
|
|
|
assert!(
|
|
matches!(
|
|
python,
|
|
PythonInstallation {
|
|
source: PythonSource::SearchPathFirst,
|
|
interpreter: _
|
|
}
|
|
),
|
|
"We should find a python; got {python:?}"
|
|
);
|
|
assert_eq!(
|
|
&python.interpreter().python_full_version().to_string(),
|
|
"3.13.0",
|
|
"We should find the correct interpreter for the request"
|
|
);
|
|
assert!(
|
|
!&python.interpreter().gil_disabled(),
|
|
"We should prefer a python with the GIL"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
}
|