mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Add uv python pin
(#4950)
Adds a `uv python pin` command to write to a `.python-version` file. We support all of our Python version request formats. We also support a `--resolved` flag to pin to a specific interpreter instead of the provided version. We canonicalize the request with #4949, it's not just printed verbatim. We always attempt to find the interpreter so we can warn if it's not available. With `--resolved`, if we can't find the interpreter we fail. If no arguments are provided, we'll attempt to display the current pin. In the future: - We should confirm that this satisfies the `Requires-Python` metadata if a `pyproject.toml` is present - We should support writing to a `uv.python-version` field if `pyproject.toml` or `uv.toml` are present - We should support finding and updating the "nearest" Python version file (looking in ancestors) - We should support finding version files in workspaces - We should support some sort of global pin
This commit is contained in:
parent
7925d255f7
commit
e0fae8e6f4
9 changed files with 622 additions and 3 deletions
|
@ -2169,6 +2169,9 @@ pub enum PythonCommand {
|
||||||
/// Search for a Python installation.
|
/// Search for a Python installation.
|
||||||
Find(PythonFindArgs),
|
Find(PythonFindArgs),
|
||||||
|
|
||||||
|
/// Pin to a specific Python version.
|
||||||
|
Pin(PythonPinArgs),
|
||||||
|
|
||||||
/// Show the uv Python installation directory.
|
/// Show the uv Python installation directory.
|
||||||
Dir,
|
Dir,
|
||||||
|
|
||||||
|
@ -2226,6 +2229,22 @@ pub struct PythonFindArgs {
|
||||||
pub request: Option<String>,
|
pub request: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
pub struct PythonPinArgs {
|
||||||
|
/// The Python version.
|
||||||
|
pub request: Option<String>,
|
||||||
|
|
||||||
|
/// Write the resolved Python interpreter path instead of the request.
|
||||||
|
///
|
||||||
|
/// Ensures that the exact same interpreter is used.
|
||||||
|
#[arg(long, overrides_with("resolved"))]
|
||||||
|
pub resolved: bool,
|
||||||
|
|
||||||
|
#[arg(long, overrides_with("no_resolved"), hide = true)]
|
||||||
|
pub no_resolved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct IndexArgs {
|
pub struct IndexArgs {
|
||||||
|
|
|
@ -28,6 +28,7 @@ pub(crate) use python::dir::dir as python_dir;
|
||||||
pub(crate) use python::find::find as python_find;
|
pub(crate) use python::find::find as python_find;
|
||||||
pub(crate) use python::install::install as python_install;
|
pub(crate) use python::install::install as python_install;
|
||||||
pub(crate) use python::list::list as python_list;
|
pub(crate) use python::list::list as python_list;
|
||||||
|
pub(crate) use python::pin::pin as python_pin;
|
||||||
pub(crate) use python::uninstall::uninstall as python_uninstall;
|
pub(crate) use python::uninstall::uninstall as python_uninstall;
|
||||||
#[cfg(feature = "self-update")]
|
#[cfg(feature = "self-update")]
|
||||||
pub(crate) use self_update::self_update;
|
pub(crate) use self_update::self_update;
|
||||||
|
|
|
@ -2,4 +2,5 @@ pub(crate) mod dir;
|
||||||
pub(crate) mod find;
|
pub(crate) mod find;
|
||||||
pub(crate) mod install;
|
pub(crate) mod install;
|
||||||
pub(crate) mod list;
|
pub(crate) mod list;
|
||||||
|
pub(crate) mod pin;
|
||||||
pub(crate) mod uninstall;
|
pub(crate) mod uninstall;
|
||||||
|
|
84
crates/uv/src/commands/python/pin.rs
Normal file
84
crates/uv/src/commands/python/pin.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
|
||||||
|
use tracing::debug;
|
||||||
|
use uv_cache::Cache;
|
||||||
|
use uv_configuration::PreviewMode;
|
||||||
|
use uv_fs::Simplified;
|
||||||
|
use uv_python::{
|
||||||
|
requests_from_version_file, EnvironmentPreference, PythonInstallation, PythonPreference,
|
||||||
|
PythonRequest, PYTHON_VERSION_FILENAME,
|
||||||
|
};
|
||||||
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
|
use crate::commands::ExitStatus;
|
||||||
|
use crate::printer::Printer;
|
||||||
|
|
||||||
|
/// Pin to a specific Python version.
|
||||||
|
pub(crate) async fn pin(
|
||||||
|
request: Option<String>,
|
||||||
|
resolved: bool,
|
||||||
|
python_preference: PythonPreference,
|
||||||
|
preview: PreviewMode,
|
||||||
|
cache: &Cache,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<ExitStatus> {
|
||||||
|
if preview.is_disabled() {
|
||||||
|
warn_user_once!("`uv python pin` is experimental and may change without warning.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(request) = request else {
|
||||||
|
// Display the current pinned Python version
|
||||||
|
if let Some(pins) = requests_from_version_file().await? {
|
||||||
|
for pin in pins {
|
||||||
|
writeln!(printer.stdout(), "{}", pin.to_canonical_string())?;
|
||||||
|
}
|
||||||
|
return Ok(ExitStatus::Success);
|
||||||
|
}
|
||||||
|
bail!("No pinned Python version found.")
|
||||||
|
};
|
||||||
|
let request = PythonRequest::parse(&request);
|
||||||
|
|
||||||
|
let python = match PythonInstallation::find(
|
||||||
|
&request,
|
||||||
|
EnvironmentPreference::OnlySystem,
|
||||||
|
python_preference,
|
||||||
|
cache,
|
||||||
|
) {
|
||||||
|
Ok(python) => Some(python),
|
||||||
|
// If no matching Python version is found, don't fail unless `resolved` was requested
|
||||||
|
Err(uv_python::Error::MissingPython(err)) if !resolved => {
|
||||||
|
warn_user_once!("{}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = if resolved {
|
||||||
|
// SAFETY: We exit early if Python is not found and resolved is `true`
|
||||||
|
python
|
||||||
|
.unwrap()
|
||||||
|
.interpreter()
|
||||||
|
.sys_executable()
|
||||||
|
.user_display()
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
request.to_canonical_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("Using pin `{}`", output);
|
||||||
|
let version_file = PathBuf::from(PYTHON_VERSION_FILENAME);
|
||||||
|
let exists = version_file.exists();
|
||||||
|
|
||||||
|
debug!("Writing pin to {}", version_file.user_display());
|
||||||
|
fs_err::write(&version_file, format!("{output}\n"))?;
|
||||||
|
if exists {
|
||||||
|
writeln!(printer.stdout(), "Replaced existing pin with `{output}`")?;
|
||||||
|
} else {
|
||||||
|
writeln!(printer.stdout(), "Pinned to `{output}`")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ExitStatus::Success)
|
||||||
|
}
|
|
@ -809,6 +809,25 @@ async fn run() -> Result<ExitStatus> {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
Commands::Python(PythonNamespace {
|
||||||
|
command: PythonCommand::Pin(args),
|
||||||
|
}) => {
|
||||||
|
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||||
|
let args = settings::PythonPinSettings::resolve(args, filesystem);
|
||||||
|
|
||||||
|
// Initialize the cache.
|
||||||
|
let cache = cache.init()?;
|
||||||
|
|
||||||
|
commands::python_pin(
|
||||||
|
args.request,
|
||||||
|
args.resolved,
|
||||||
|
globals.python_preference,
|
||||||
|
globals.preview,
|
||||||
|
&cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
Commands::Python(PythonNamespace {
|
Commands::Python(PythonNamespace {
|
||||||
command: PythonCommand::Dir,
|
command: PythonCommand::Dir,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
|
@ -14,8 +14,8 @@ use uv_cli::{
|
||||||
AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, ListFormat, LockArgs, Maybe,
|
AddArgs, ColorChoice, Commands, ExternalCommand, GlobalArgs, ListFormat, LockArgs, Maybe,
|
||||||
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
||||||
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
|
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
|
||||||
PythonUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs,
|
PythonPinArgs, PythonUninstallArgs, RemoveArgs, RunArgs, SyncArgs, ToolInstallArgs,
|
||||||
ToolUninstallArgs, TreeArgs, VenvArgs,
|
ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, VenvArgs,
|
||||||
};
|
};
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
|
@ -410,6 +410,30 @@ impl PythonFindSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The resolved settings to use for a `python pin` invocation.
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct PythonPinSettings {
|
||||||
|
pub(crate) request: Option<String>,
|
||||||
|
pub(crate) resolved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PythonPinSettings {
|
||||||
|
/// Resolve the [`PythonPinSettings`] from the CLI and workspace configuration.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub(crate) fn resolve(args: PythonPinArgs, _filesystem: Option<FilesystemOptions>) -> Self {
|
||||||
|
let PythonPinArgs {
|
||||||
|
request,
|
||||||
|
no_resolved,
|
||||||
|
resolved,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
request,
|
||||||
|
resolved: flag(resolved, no_resolved).unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// The resolved settings to use for a `sync` invocation.
|
/// The resolved settings to use for a `sync` invocation.
|
||||||
#[allow(clippy::struct_excessive_bools, dead_code)]
|
#[allow(clippy::struct_excessive_bools, dead_code)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -233,7 +233,12 @@ impl TestContext {
|
||||||
filters.extend(
|
filters.extend(
|
||||||
Self::path_patterns(&python_dir.join(version.to_string()))
|
Self::path_patterns(&python_dir.join(version.to_string()))
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|pattern| (format!("{pattern}.*"), format!("[PYTHON-{version}]"))),
|
.map(|pattern| {
|
||||||
|
(
|
||||||
|
format!("{pattern}[a-zA-Z0-9]*"),
|
||||||
|
format!("[PYTHON-{version}]"),
|
||||||
|
)
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add Python patch version filtering unless explicitly requested to ensure
|
// Add Python patch version filtering unless explicitly requested to ensure
|
||||||
|
@ -429,6 +434,19 @@ impl TestContext {
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a `uv python pin` command with options shared across scenarios.
|
||||||
|
pub fn python_pin(&self) -> Command {
|
||||||
|
let mut command = Command::new(get_bin());
|
||||||
|
command
|
||||||
|
.arg("python")
|
||||||
|
.arg("pin")
|
||||||
|
.env("UV_PREVIEW", "1")
|
||||||
|
.env("UV_PYTHON_INSTALL_DIR", "")
|
||||||
|
.current_dir(&self.temp_dir);
|
||||||
|
self.add_shared_args(&mut command);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a `uv python dir` command with options shared across scenarios.
|
/// Create a `uv python dir` command with options shared across scenarios.
|
||||||
pub fn python_dir(&self) -> Command {
|
pub fn python_dir(&self) -> Command {
|
||||||
let mut command = Command::new(get_bin());
|
let mut command = Command::new(get_bin());
|
||||||
|
|
|
@ -186,6 +186,7 @@ fn help_subcommand() {
|
||||||
list List the available Python installations
|
list List the available Python installations
|
||||||
install Download and install Python versions
|
install Download and install Python versions
|
||||||
find Search for a Python installation
|
find Search for a Python installation
|
||||||
|
pin Pin to a specific Python version
|
||||||
dir Show the uv Python installation directory
|
dir Show the uv Python installation directory
|
||||||
uninstall Uninstall Python versions
|
uninstall Uninstall Python versions
|
||||||
|
|
||||||
|
@ -406,6 +407,7 @@ fn help_flag_subcommand() {
|
||||||
list List the available Python installations
|
list List the available Python installations
|
||||||
install Download and install Python versions
|
install Download and install Python versions
|
||||||
find Search for a Python installation
|
find Search for a Python installation
|
||||||
|
pin Pin to a specific Python version
|
||||||
dir Show the uv Python installation directory
|
dir Show the uv Python installation directory
|
||||||
uninstall Uninstall Python versions
|
uninstall Uninstall Python versions
|
||||||
|
|
||||||
|
@ -543,6 +545,7 @@ fn help_unknown_subsubcommand() {
|
||||||
list
|
list
|
||||||
install
|
install
|
||||||
find
|
find
|
||||||
|
pin
|
||||||
dir
|
dir
|
||||||
uninstall
|
uninstall
|
||||||
"###);
|
"###);
|
||||||
|
|
450
crates/uv/tests/python_pin.rs
Normal file
450
crates/uv/tests/python_pin.rs
Normal file
|
@ -0,0 +1,450 @@
|
||||||
|
#![cfg(all(feature = "python", feature = "pypi"))]
|
||||||
|
|
||||||
|
use common::{uv_snapshot, TestContext};
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
use uv_python::{
|
||||||
|
platform::{Arch, Os},
|
||||||
|
PYTHON_VERSION_FILENAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn python_pin() {
|
||||||
|
let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]);
|
||||||
|
|
||||||
|
// Without arguments, we attempt to read the current pin (which does not exist yet)
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin(), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: No pinned Python version found.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Given an argument, we pin to that version
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("any"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Pinned to `any`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r#"any"#);
|
||||||
|
|
||||||
|
// Without arguments, we read the current pin
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin(), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
any
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// We should not mutate the file
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r#"any"#);
|
||||||
|
|
||||||
|
// Request Python 3.12
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `3.12`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
3.12
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request Python 3.11
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("3.11"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `3.11`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
3.11
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request CPython
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("cpython"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `cpython`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
cpython
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request CPython 3.12
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("cpython@3.12"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `cpython@3.12`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
cpython@3.12
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request CPython 3.12 via non-canonical syntax
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("cp3.12"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `cpython@3.12`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
cpython@3.12
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request CPython 3.12 via partial key syntax
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("cpython-3.12"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `cpython-3.12-any-any-any`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
cpython-3.12-any-any-any
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Request a specific path
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg(&context.python_versions.first().unwrap().1), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `[PYTHON-3.11]`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.11]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request an implementation that is not installed
|
||||||
|
// (skip on Windows because the snapshot is different and the behavior is not platform dependent)
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("pypy"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `pypy`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: No interpreter found for PyPy in system path
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
pypy
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request a version that is not installed
|
||||||
|
// (skip on Windows because the snapshot is different and the behavior is not platform dependent)
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("3.7"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `3.7`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: No interpreter found for Python 3.7 in system path
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
3.7
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We do not need a Python interpreter to pin without `--resolved`
|
||||||
|
/// (skip on Windows because the snapshot is different and the behavior is not platform dependent)
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn python_pin_no_python() {
|
||||||
|
let context: TestContext = TestContext::new_with_versions(&[]);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Pinned to `3.12`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: No interpreter found for Python 3.12 in system path
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We do need a Python interpreter for `--resolved` pins
|
||||||
|
#[test]
|
||||||
|
fn python_pin_resolve_no_python() {
|
||||||
|
let context: TestContext = TestContext::new_with_versions(&[]);
|
||||||
|
|
||||||
|
if cfg!(windows) {
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.12"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: No interpreter found for Python 3.12 in system path or `py` launcher
|
||||||
|
"###);
|
||||||
|
} else {
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.12"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: No interpreter found for Python 3.12 in system path
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn python_pin_resolve() {
|
||||||
|
let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]);
|
||||||
|
|
||||||
|
// We pin the first interpreter on the path
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("any"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Pinned to `[PYTHON-3.11]`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.11]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request Python 3.12
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.12"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `[PYTHON-3.12]`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.12]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request Python 3.11
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.11"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `[PYTHON-3.11]`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.11]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request CPython
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("cpython"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `[PYTHON-3.11]`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.11]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request CPython 3.12
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("cpython@3.12"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `[PYTHON-3.12]`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.12]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request CPython 3.12 via partial key syntax
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("cpython-3.12"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `[PYTHON-3.12]`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.12]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request CPython 3.12 for the current platform
|
||||||
|
let os = Os::from_env();
|
||||||
|
let arch = Arch::from_env();
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved")
|
||||||
|
.arg(format!("cpython-3.12-{os}-{arch}"))
|
||||||
|
, @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Replaced existing pin with `[PYTHON-3.12]`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.12]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request an implementation that is not installed
|
||||||
|
// (skip on Windows because the snapshot is different and the behavior is not platform dependent)
|
||||||
|
#[cfg(unix)]
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("pypy"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: No interpreter found for PyPy in system path
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.12]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request a version that is not installed
|
||||||
|
// (skip on Windows because the snapshot is different and the behavior is not platform dependent)
|
||||||
|
#[cfg(unix)]
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.7"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: No interpreter found for Python 3.7 in system path
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let python_version =
|
||||||
|
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(python_version, @r###"
|
||||||
|
[PYTHON-3.12]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue