use std::path::PathBuf;
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir};
use insta::assert_snapshot;
use uv_platform::{Arch, Os};
use uv_python::{PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME};
use uv_static::EnvVars;
use uv_test::uv_snapshot;
#[test]
fn python_pin() {
let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]);
uv_snapshot!(context.filters(), context.python_pin(), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No Python version file found; specify a version to create one
");
uv_snapshot!(context.filters(), context.python_pin().arg("any"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `any`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"any");
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
any
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"any");
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `any` -> `3.12`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"3.12");
uv_snapshot!(context.filters(), context.python_pin().arg("3.11"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `3.12` -> `3.11`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"3.11");
uv_snapshot!(context.filters(), context.python_pin().arg("cpython"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `3.11` -> `cpython`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"cpython");
uv_snapshot!(context.filters(), context.python_pin().arg("cpython@3.12"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `cpython` -> `cpython@3.12`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"cpython@3.12");
uv_snapshot!(context.filters(), context.python_pin().arg("cp3.12"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `cpython@3.12`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"cpython@3.12");
uv_snapshot!(context.filters(), context.python_pin().arg("cpython-3.12"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `cpython@3.12` -> `cpython-3.12-any-any-any`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"cpython-3.12-any-any-any");
uv_snapshot!(context.filters(), context.python_pin().arg(&context.python_versions.first().unwrap().1), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `cpython-3.12-any-any-any` -> `[PYTHON-3.11]`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.11]");
});
#[cfg(unix)]
{
uv_snapshot!(context.filters(), context.python_pin().arg("pypy"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `[PYTHON-3.11]` -> `pypy`
----- stderr -----
warning: No interpreter found for PyPy in managed installations or search path
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"pypy");
}
#[cfg(unix)]
{
uv_snapshot!(context.filters(), context.python_pin().arg("3.7"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `pypy` -> `3.7`
----- stderr -----
warning: No interpreter found for Python 3.7 in managed installations or search path
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(python_version, @"3.7");
}
}
#[test]
fn python_pin_uses_python_downloads_json_url() {
let context = uv_test::test_context_with_versions!(&[]).with_filtered_python_sources();
let metadata = context.temp_dir.child("empty-download-metadata.json");
metadata.write_str("{}").unwrap();
uv_snapshot!(context.filters(), context
.python_pin()
.arg("--resolved")
.arg("3.12")
.arg("--python-downloads-json-url")
.arg(metadata.path()), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No interpreter found for Python 3.12 in [PYTHON SOURCES]
");
}
#[test]
fn python_pin_global_if_no_local() -> Result<()> {
let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]);
let uv = context.user_config_dir.child("uv");
uv.create_dir_all()?;
uv_snapshot!(context.filters(), context.python_pin(), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No Python version file found; specify a version to create one
");
uv_snapshot!(context.filters(), context.python_pin().arg("3.11").arg("--global"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `[UV_USER_CONFIG_DIR]/.python-version` to `3.11`
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
3.11
----- stderr -----
");
Ok(())
}
#[test]
fn python_pin_global_use_local_if_available() -> Result<()> {
let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]);
let uv = context.user_config_dir.child("uv");
uv.create_dir_all()?;
uv_snapshot!(context.filters(), context.python_pin().arg("3.12").arg("--global"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `[UV_USER_CONFIG_DIR]/.python-version` to `3.12`
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
3.12
----- stderr -----
");
let mut global_version_path = PathBuf::from(uv.path());
global_version_path.push(PYTHON_VERSION_FILENAME);
let global_python_version = context.read(&global_version_path);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(global_python_version, @"3.12");
});
uv_snapshot!(context.filters(), context.python_pin().arg("3.11"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `3.11`
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
3.11
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_pin().arg("--global"), @"
success: true
exit_code: 0
----- stdout -----
3.12
----- stderr -----
");
let local_python_version = context.read(PYTHON_VERSION_FILENAME);
assert_snapshot!(local_python_version, @"3.11");
let global_python_version = context.read(&global_version_path);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(global_python_version, @"3.12");
});
Ok(())
}
#[test]
fn python_pin_global_creates_parent_dirs() {
let context = uv_test::test_context_with_versions!(&["3.12"]);
let uv_global_config_dir = context.user_config_dir.child("uv");
assert!(
!uv_global_config_dir.exists(),
"Global config directory should not exist yet."
);
uv_snapshot!(context.filters(), context.python_pin().arg("3.12").arg("--global"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `[UV_USER_CONFIG_DIR]/.python-version` to `3.12`
----- stderr -----
");
assert!(
uv_global_config_dir.exists(),
"Global config directory should be automatically created (if missing) after global pin."
);
}
#[cfg(unix)]
#[test]
fn python_pin_no_python() {
let context = uv_test::test_context_with_versions!(&[]);
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `3.12`
----- stderr -----
warning: No interpreter found for Python 3.12 in managed installations or search path
");
}
#[test]
fn python_pin_compatible_with_requires_python() -> Result<()> {
let context =
uv_test::test_context_with_versions!(&["3.10", "3.11"]).with_filtered_python_sources();
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["iniconfig"]
"#,
)?;
uv_snapshot!(context.filters(), context.python_pin().arg("3.10"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: The requested Python version `3.10` is incompatible with the project `requires-python` value of `>=3.11`.
");
uv_snapshot!(context.filters(), context.python_pin().arg("cpython@3.10"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: The requested Python version `cpython@3.10` is incompatible with the project `requires-python` value of `>=3.11`.
");
uv_snapshot!(context.filters(), context.python_pin().arg("cpython@3.10").arg("--no-project"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `cpython@3.10`
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_pin().arg("cpython@3.10").arg("--no-workspace"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `cpython@3.10`
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_pin().arg(">3.8,<3.11"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `cpython@3.10` -> `>3.8, <3.11`
----- stderr -----
warning: The requested Python version `>3.8, <3.11` resolves to `3.10.[X]` which is incompatible with the project `requires-python` value of `>=3.11`.
");
uv_snapshot!(context.filters(), context.python_pin().arg("3.11"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `>3.8, <3.11` -> `3.11`
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_pin().arg("3.13t"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `3.11` -> `3.13+freethreaded`
----- stderr -----
warning: No interpreter found for Python 3.13+freethreaded in [PYTHON SOURCES]
");
uv_snapshot!(context.filters(), context.python_pin().arg("cpython@3.11"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `3.13+freethreaded` -> `cpython@3.11`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"cpython@3.11");
});
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig"]
"#,
)?;
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
cpython@3.11
----- stderr -----
warning: The pinned Python version `cpython@3.11` is incompatible with the project `requires-python` value of `>=3.12`.
");
uv_snapshot!(context.filters(), context.python_pin().arg("cpython"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `cpython@3.11` -> `cpython`
----- stderr -----
warning: The requested Python version `cpython` resolves to `3.10.[X]` which is incompatible with the project `requires-python` value of `>=3.12`.
");
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
cpython
----- stderr -----
warning: The pinned Python version `cpython` resolves to `3.10.[X]` which is incompatible with the project `requires-python` value of `>=3.12`.
");
uv_snapshot!(context.filters(), context.python_pin().arg(">3.8,<3.12"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `cpython` -> `>3.8, <3.12`
----- stderr -----
warning: The requested Python version `>3.8, <3.12` resolves to `3.10.[X]` which is incompatible with the project `requires-python` value of `>=3.12`.
");
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
>3.8, <3.12
----- stderr -----
warning: The pinned Python version `>3.8, <3.12` resolves to `3.10.[X]` which is incompatible with the project `requires-python` value of `>=3.12`.
");
Ok(())
}
#[test]
fn warning_pinned_python_version_not_installed() -> Result<()> {
let context = uv_test::test_context_with_versions!(&["3.10", "3.11"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["iniconfig"]
"#,
)?;
let python_version_file = context.temp_dir.child(PYTHON_VERSION_FILENAME);
python_version_file.write_str(r"3.12")?;
if cfg!(windows) {
uv_snapshot!(context.filters(), context.python_pin(), @r###"
success: true
exit_code: 0
----- stdout -----
3.12
----- stderr -----
warning: Failed to resolve pinned Python version `3.12`: No interpreter found for Python 3.12 in managed installations, search path, or registry
"###);
} else {
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
3.12
----- stderr -----
warning: Failed to resolve pinned Python version `3.12`: No interpreter found for Python 3.12 in managed installations or search path
");
}
Ok(())
}
#[test]
fn python_pin_resolve_no_python() {
let context = uv_test::test_context_with_versions!(&[]).with_filtered_python_sources();
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.12"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No interpreter found for Python 3.12 in [PYTHON SOURCES]
hint: A managed Python download is available for Python 3.12, but Python downloads are set to 'never'
");
}
#[test]
fn python_pin_resolve() {
let context = uv_test::test_context_with_versions!(&["3.12", "3.13"]);
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("any"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `[PYTHON-3.12]`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.12]");
});
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.13"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `[PYTHON-3.12]` -> `[PYTHON-3.13]`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.13]");
});
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.13"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `[PYTHON-3.13]`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.13]");
});
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("cpython"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `[PYTHON-3.13]` -> `[PYTHON-3.12]`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.12]");
});
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("cpython@3.13"), @"
success: true
exit_code: 0
----- stdout -----
Updated `.python-version` from `[PYTHON-3.12]` -> `[PYTHON-3.13]`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.13]");
});
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("cpython-3.13"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `[PYTHON-3.13]`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.13]");
});
let os = Os::from_env();
let arch = Arch::from_env();
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved")
.arg(format!("cpython-3.13-{os}-{arch}"))
, @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `[PYTHON-3.13]`
----- stderr -----
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.13]");
});
#[cfg(unix)]
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("pypy"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No interpreter found for PyPy in managed installations or search path
hint: A managed Python download is available for PyPy, but Python downloads are set to 'never'
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.13]");
});
#[cfg(unix)]
uv_snapshot!(context.filters(), context.python_pin().arg("--resolved").arg("3.7"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No interpreter found for Python 3.7 in managed installations or search path
");
let python_version = context.read(PYTHON_VERSION_FILENAME);
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(python_version, @"[PYTHON-3.13]");
});
}
#[test]
fn python_pin_with_comments() -> Result<()> {
let context = uv_test::test_context_with_versions!(&[]);
let content = indoc::indoc! {r"
3.12
# 3.11
3.10
"};
let version_file = context.temp_dir.child(PYTHON_VERSION_FILENAME);
version_file.write_str(content)?;
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
3.12
3.10
----- stderr -----
");
fs_err::remove_file(version_file)?;
let versions_file = context.temp_dir.child(PYTHON_VERSIONS_FILENAME);
versions_file.write_str(content)?;
uv_snapshot!(context.filters(), context.python_pin(), @"
success: true
exit_code: 0
----- stdout -----
3.12
3.10
----- stderr -----
");
Ok(())
}
#[test]
#[cfg(feature = "test-python-managed")]
fn python_pin_install() {
let context = uv_test::test_context_with_versions!(&[]).with_filtered_python_sources();
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `3.12`
----- stderr -----
warning: No interpreter found for Python 3.12 in [PYTHON SOURCES]
");
uv_snapshot!(context.filters(), context.python_pin().arg("3.12").env(EnvVars::UV_PYTHON_DOWNLOADS, "auto"), @"
success: true
exit_code: 0
----- stdout -----
Pinned `.python-version` to `3.12`
----- stderr -----
");
}
#[test]
fn python_pin_rm() {
let context = uv_test::test_context_with_versions!(&["3.12"]);
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No Python version file found
");
context.python_pin().arg("3.12").assert().success();
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @"
success: true
exit_code: 0
----- stdout -----
Removed Python version file at `.python-version`
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_pin().arg("--rm").arg("--global"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No global Python pin found
");
context.python_pin().arg("3.12").assert().success();
uv_snapshot!(context.filters(), context.python_pin().arg("--rm").arg("--global"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No global Python pin found
");
context
.python_pin()
.arg("3.12")
.arg("--global")
.assert()
.success();
uv_snapshot!(context.filters(), context.python_pin().arg("--rm").arg("--global"), @"
success: true
exit_code: 0
----- stdout -----
Removed global Python pin at `[UV_USER_CONFIG_DIR]/.python-version`
----- stderr -----
");
context
.python_pin()
.arg("3.12")
.arg("--global")
.assert()
.success();
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @"
success: true
exit_code: 0
----- stdout -----
Removed Python version file at `.python-version`
----- stderr -----
");
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No Python version file found; use `--rm --global` to remove the global pin
");
}