mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-08 05:45:00 +00:00
Add support for global uv python pin
(#12115)
These changes add support for ``` uv python pin 3.12 --global ``` This adds the specified version to a `.python-version` file in the user-level config directory. uv will now use the user-level version as a fallback if no version is found in the project directory or its ancestors. Closes #4972
This commit is contained in:
parent
b4eabf9a61
commit
797f1fbac0
17 changed files with 366 additions and 16 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4579,6 +4579,7 @@ dependencies = [
|
||||||
"uv-client",
|
"uv-client",
|
||||||
"uv-configuration",
|
"uv-configuration",
|
||||||
"uv-console",
|
"uv-console",
|
||||||
|
"uv-dirs",
|
||||||
"uv-dispatch",
|
"uv-dispatch",
|
||||||
"uv-distribution",
|
"uv-distribution",
|
||||||
"uv-distribution-filename",
|
"uv-distribution-filename",
|
||||||
|
|
|
@ -4676,6 +4676,19 @@ pub struct PythonPinArgs {
|
||||||
/// `requires-python` constraint.
|
/// `requires-python` constraint.
|
||||||
#[arg(long, alias = "no-workspace")]
|
#[arg(long, alias = "no-workspace")]
|
||||||
pub no_project: bool,
|
pub no_project: bool,
|
||||||
|
|
||||||
|
/// Update the global Python version pin.
|
||||||
|
///
|
||||||
|
/// Writes the pinned Python version to a `.python-version` file in the uv user configuration
|
||||||
|
/// directory: `XDG_CONFIG_HOME/uv` on Linux/macOS and `%APPDATA%/uv` on Windows.
|
||||||
|
///
|
||||||
|
/// When a local Python version pin is not found in the working directory or an ancestor
|
||||||
|
/// directory, this version will be used instead.
|
||||||
|
///
|
||||||
|
/// Unlike local version pins, this version is used as the default for commands that mutate
|
||||||
|
/// global state, like `uv tool install`.
|
||||||
|
#[arg(long)]
|
||||||
|
pub global: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
|
@ -103,6 +103,13 @@ pub fn user_config_dir() -> Option<PathBuf> {
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn user_uv_config_dir() -> Option<PathBuf> {
|
||||||
|
user_config_dir().map(|mut path| {
|
||||||
|
path.push("uv");
|
||||||
|
path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn locate_system_config_xdg(value: Option<&str>) -> Option<PathBuf> {
|
fn locate_system_config_xdg(value: Option<&str>) -> Option<PathBuf> {
|
||||||
// On Linux and macOS, read the `XDG_CONFIG_DIRS` environment variable.
|
// On Linux and macOS, read the `XDG_CONFIG_DIRS` environment variable.
|
||||||
|
|
|
@ -149,8 +149,6 @@ impl Conflicts {
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(topo_nodes) = toposort(&graph, None) else {
|
let Ok(topo_nodes) = toposort(&graph, None) else {
|
||||||
// FIXME: If we hit a cycle, we are currently bailing and waiting for
|
|
||||||
// more detailed cycle detection downstream. Is this what we want?
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// Propagate canonical items through the graph and populate substitutions.
|
// Propagate canonical items through the graph and populate substitutions.
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
|
||||||
use fs_err as fs;
|
use fs_err as fs;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
use uv_dirs::user_uv_config_dir;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
|
|
||||||
use crate::PythonRequest;
|
use crate::PythonRequest;
|
||||||
|
@ -69,7 +70,13 @@ impl PythonVersionFile {
|
||||||
options: &DiscoveryOptions<'_>,
|
options: &DiscoveryOptions<'_>,
|
||||||
) -> Result<Option<Self>, std::io::Error> {
|
) -> Result<Option<Self>, std::io::Error> {
|
||||||
let Some(path) = Self::find_nearest(working_directory, options) else {
|
let Some(path) = Self::find_nearest(working_directory, options) else {
|
||||||
return Ok(None);
|
// Not found in directory or its ancestors. Looking in user-level config.
|
||||||
|
return Ok(match user_uv_config_dir() {
|
||||||
|
Some(user_dir) => Self::discover_user_config(user_dir, options)
|
||||||
|
.await?
|
||||||
|
.or(None),
|
||||||
|
None => None,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if options.no_config {
|
if options.no_config {
|
||||||
|
@ -84,6 +91,22 @@ impl PythonVersionFile {
|
||||||
Self::try_from_path(path).await
|
Self::try_from_path(path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn discover_user_config(
|
||||||
|
user_config_working_directory: impl AsRef<Path>,
|
||||||
|
options: &DiscoveryOptions<'_>,
|
||||||
|
) -> Result<Option<Self>, std::io::Error> {
|
||||||
|
if !options.no_config {
|
||||||
|
if let Some(path) =
|
||||||
|
Self::find_in_directory(user_config_working_directory.as_ref(), options)
|
||||||
|
.into_iter()
|
||||||
|
.find(|path| path.is_file())
|
||||||
|
{
|
||||||
|
return Self::try_from_path(path).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
fn find_nearest(path: impl AsRef<Path>, options: &DiscoveryOptions<'_>) -> Option<PathBuf> {
|
fn find_nearest(path: impl AsRef<Path>, options: &DiscoveryOptions<'_>) -> Option<PathBuf> {
|
||||||
path.as_ref()
|
path.as_ref()
|
||||||
.ancestors()
|
.ancestors()
|
||||||
|
|
|
@ -347,6 +347,12 @@ impl EnvVars {
|
||||||
/// Path to system-level configuration directory on Windows systems.
|
/// Path to system-level configuration directory on Windows systems.
|
||||||
pub const SYSTEMDRIVE: &'static str = "SYSTEMDRIVE";
|
pub const SYSTEMDRIVE: &'static str = "SYSTEMDRIVE";
|
||||||
|
|
||||||
|
/// Path to user-level configuration directory on Windows systems.
|
||||||
|
pub const APPDATA: &'static str = "APPDATA";
|
||||||
|
|
||||||
|
/// Path to root directory of user's profile on Windows systems.
|
||||||
|
pub const USERPROFILE: &'static str = "USERPROFILE";
|
||||||
|
|
||||||
/// Path to user-level configuration directory on Unix systems.
|
/// Path to user-level configuration directory on Unix systems.
|
||||||
pub const XDG_CONFIG_HOME: &'static str = "XDG_CONFIG_HOME";
|
pub const XDG_CONFIG_HOME: &'static str = "XDG_CONFIG_HOME";
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ uv-cli = { workspace = true }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
uv-configuration = { workspace = true }
|
uv-configuration = { workspace = true }
|
||||||
uv-console = { workspace = true }
|
uv-console = { workspace = true }
|
||||||
|
uv-dirs = { workspace = true }
|
||||||
uv-dispatch = { workspace = true }
|
uv-dispatch = { workspace = true }
|
||||||
uv-distribution = { workspace = true }
|
uv-distribution = { workspace = true }
|
||||||
uv-distribution-filename = { workspace = true }
|
uv-distribution-filename = { workspace = true }
|
||||||
|
|
|
@ -7,6 +7,7 @@ use owo_colors::OwoColorize;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
|
use uv_dirs::user_uv_config_dir;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_python::{
|
use uv_python::{
|
||||||
EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile,
|
EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile,
|
||||||
|
@ -25,6 +26,7 @@ pub(crate) async fn pin(
|
||||||
resolved: bool,
|
resolved: bool,
|
||||||
python_preference: PythonPreference,
|
python_preference: PythonPreference,
|
||||||
no_project: bool,
|
no_project: bool,
|
||||||
|
global: bool,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
|
@ -43,8 +45,16 @@ pub(crate) async fn pin(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let version_file =
|
let version_file = if global {
|
||||||
PythonVersionFile::discover(project_dir, &VersionFileDiscoveryOptions::default()).await;
|
if let Some(path) = user_uv_config_dir() {
|
||||||
|
PythonVersionFile::discover_user_config(path, &VersionFileDiscoveryOptions::default())
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PythonVersionFile::discover(project_dir, &VersionFileDiscoveryOptions::default()).await
|
||||||
|
};
|
||||||
|
|
||||||
let Some(request) = request else {
|
let Some(request) = request else {
|
||||||
// Display the current pinned Python version
|
// Display the current pinned Python version
|
||||||
|
@ -130,8 +140,16 @@ pub(crate) async fn pin(
|
||||||
|
|
||||||
let existing = version_file.ok().flatten();
|
let existing = version_file.ok().flatten();
|
||||||
// TODO(zanieb): Allow updating the discovered version file with an `--update` flag.
|
// TODO(zanieb): Allow updating the discovered version file with an `--update` flag.
|
||||||
let new = PythonVersionFile::new(project_dir.join(PYTHON_VERSION_FILENAME))
|
let new = if global {
|
||||||
.with_versions(vec![request]);
|
let Some(config_dir) = user_uv_config_dir() else {
|
||||||
|
return Err(anyhow::anyhow!("No user-level config directory found."));
|
||||||
|
};
|
||||||
|
PythonVersionFile::new(config_dir.join(PYTHON_VERSION_FILENAME))
|
||||||
|
.with_versions(vec![request])
|
||||||
|
} else {
|
||||||
|
PythonVersionFile::new(project_dir.join(PYTHON_VERSION_FILENAME))
|
||||||
|
.with_versions(vec![request])
|
||||||
|
};
|
||||||
|
|
||||||
new.write().await?;
|
new.write().await?;
|
||||||
|
|
||||||
|
|
|
@ -1257,6 +1257,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
args.resolved,
|
args.resolved,
|
||||||
globals.python_preference,
|
globals.python_preference,
|
||||||
args.no_project,
|
args.no_project,
|
||||||
|
args.global,
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
|
|
|
@ -979,6 +979,7 @@ pub(crate) struct PythonPinSettings {
|
||||||
pub(crate) request: Option<String>,
|
pub(crate) request: Option<String>,
|
||||||
pub(crate) resolved: bool,
|
pub(crate) resolved: bool,
|
||||||
pub(crate) no_project: bool,
|
pub(crate) no_project: bool,
|
||||||
|
pub(crate) global: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PythonPinSettings {
|
impl PythonPinSettings {
|
||||||
|
@ -990,12 +991,14 @@ impl PythonPinSettings {
|
||||||
no_resolved,
|
no_resolved,
|
||||||
resolved,
|
resolved,
|
||||||
no_project,
|
no_project,
|
||||||
|
global,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
request,
|
request,
|
||||||
resolved: flag(resolved, no_resolved).unwrap_or(false),
|
resolved: flag(resolved, no_resolved).unwrap_or(false),
|
||||||
no_project,
|
no_project,
|
||||||
|
global,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,7 @@ pub struct TestContext {
|
||||||
pub cache_dir: ChildPath,
|
pub cache_dir: ChildPath,
|
||||||
pub python_dir: ChildPath,
|
pub python_dir: ChildPath,
|
||||||
pub home_dir: ChildPath,
|
pub home_dir: ChildPath,
|
||||||
|
pub user_config_dir: ChildPath,
|
||||||
pub bin_dir: ChildPath,
|
pub bin_dir: ChildPath,
|
||||||
pub venv: ChildPath,
|
pub venv: ChildPath,
|
||||||
pub workspace_root: PathBuf,
|
pub workspace_root: PathBuf,
|
||||||
|
@ -357,6 +358,12 @@ impl TestContext {
|
||||||
let home_dir = ChildPath::new(root.path()).child("home");
|
let home_dir = ChildPath::new(root.path()).child("home");
|
||||||
fs_err::create_dir_all(&home_dir).expect("Failed to create test home directory");
|
fs_err::create_dir_all(&home_dir).expect("Failed to create test home directory");
|
||||||
|
|
||||||
|
let user_config_dir = if cfg!(windows) {
|
||||||
|
ChildPath::new(home_dir.path())
|
||||||
|
} else {
|
||||||
|
ChildPath::new(home_dir.path()).child(".config")
|
||||||
|
};
|
||||||
|
|
||||||
// Canonicalize the temp dir for consistent snapshot behavior
|
// Canonicalize the temp dir for consistent snapshot behavior
|
||||||
let canonical_temp_dir = temp_dir.canonicalize().unwrap();
|
let canonical_temp_dir = temp_dir.canonicalize().unwrap();
|
||||||
let venv = ChildPath::new(canonical_temp_dir.join(".venv"));
|
let venv = ChildPath::new(canonical_temp_dir.join(".venv"));
|
||||||
|
@ -472,6 +479,18 @@ impl TestContext {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|pattern| (pattern, "[PYTHON_DIR]/".to_string())),
|
.map(|pattern| (pattern, "[PYTHON_DIR]/".to_string())),
|
||||||
);
|
);
|
||||||
|
let mut uv_user_config_dir = PathBuf::from(user_config_dir.path());
|
||||||
|
uv_user_config_dir.push("uv");
|
||||||
|
filters.extend(
|
||||||
|
Self::path_patterns(&uv_user_config_dir)
|
||||||
|
.into_iter()
|
||||||
|
.map(|pattern| (pattern, "[UV_USER_CONFIG_DIR]/".to_string())),
|
||||||
|
);
|
||||||
|
filters.extend(
|
||||||
|
Self::path_patterns(&user_config_dir)
|
||||||
|
.into_iter()
|
||||||
|
.map(|pattern| (pattern, "[USER_CONFIG_DIR]/".to_string())),
|
||||||
|
);
|
||||||
filters.extend(
|
filters.extend(
|
||||||
Self::path_patterns(&home_dir)
|
Self::path_patterns(&home_dir)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -532,6 +551,7 @@ impl TestContext {
|
||||||
cache_dir,
|
cache_dir,
|
||||||
python_dir,
|
python_dir,
|
||||||
home_dir,
|
home_dir,
|
||||||
|
user_config_dir,
|
||||||
bin_dir,
|
bin_dir,
|
||||||
venv,
|
venv,
|
||||||
workspace_root,
|
workspace_root,
|
||||||
|
@ -606,6 +626,8 @@ impl TestContext {
|
||||||
.env(EnvVars::COLUMNS, "100")
|
.env(EnvVars::COLUMNS, "100")
|
||||||
.env(EnvVars::PATH, path)
|
.env(EnvVars::PATH, path)
|
||||||
.env(EnvVars::HOME, self.home_dir.as_os_str())
|
.env(EnvVars::HOME, self.home_dir.as_os_str())
|
||||||
|
.env(EnvVars::APPDATA, self.home_dir.as_os_str())
|
||||||
|
.env(EnvVars::USERPROFILE, self.home_dir.as_os_str())
|
||||||
.env(EnvVars::UV_PYTHON_INSTALL_DIR, "")
|
.env(EnvVars::UV_PYTHON_INSTALL_DIR, "")
|
||||||
// Installations are not allowed by default; see `Self::with_managed_python_dirs`
|
// Installations are not allowed by default; see `Self::with_managed_python_dirs`
|
||||||
.env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
|
.env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
|
||||||
|
@ -616,6 +638,7 @@ impl TestContext {
|
||||||
.env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1")
|
.env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1")
|
||||||
.env_remove(EnvVars::UV_CACHE_DIR)
|
.env_remove(EnvVars::UV_CACHE_DIR)
|
||||||
.env_remove(EnvVars::UV_TOOL_BIN_DIR)
|
.env_remove(EnvVars::UV_TOOL_BIN_DIR)
|
||||||
|
.env_remove(EnvVars::XDG_CONFIG_HOME)
|
||||||
.current_dir(self.temp_dir.path());
|
.current_dir(self.temp_dir.path());
|
||||||
|
|
||||||
for (key, value) in &self.extra_env {
|
for (key, value) in &self.extra_env {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::common::{uv_snapshot, TestContext};
|
use crate::common::{uv_snapshot, TestContext};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assert_fs::fixture::{FileWriteStr, PathChild};
|
use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir};
|
||||||
use insta::assert_snapshot;
|
use insta::assert_snapshot;
|
||||||
use uv_python::{
|
use uv_python::{
|
||||||
platform::{Arch, Os},
|
platform::{Arch, Os},
|
||||||
|
@ -198,6 +200,134 @@ fn python_pin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there is no project-level `.python-version` file, respect the global pin.
|
||||||
|
#[test]
|
||||||
|
fn python_pin_global_if_no_local() -> Result<()> {
|
||||||
|
let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]);
|
||||||
|
let uv = context.user_config_dir.child("uv");
|
||||||
|
uv.create_dir_all()?;
|
||||||
|
|
||||||
|
// 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 globally pin to that version
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("3.11").arg("--global"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Pinned `[UV_USER_CONFIG_DIR]/.python-version` to `3.11`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
// If no local pin, use global.
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin(), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
3.11
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a project-level `.python-version` file, it takes precedence over
|
||||||
|
// the global pin.
|
||||||
|
#[test]
|
||||||
|
fn python_pin_global_use_local_if_available() -> Result<()> {
|
||||||
|
let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]);
|
||||||
|
let uv = context.user_config_dir.child("uv");
|
||||||
|
uv.create_dir_all()?;
|
||||||
|
|
||||||
|
// Given an argument, we globally pin to that version
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("3.12").arg("--global"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Pinned `[UV_USER_CONFIG_DIR]/.python-version` to `3.12`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
// With no local, we get the global pin
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin(), @r###"
|
||||||
|
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, @r###"
|
||||||
|
3.12
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request Python 3.11 for local .python-version
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("3.11"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Pinned `.python-version` to `3.11`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Local should override global
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin(), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
3.11
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// We should still be able to check global pin
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("--global"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
3.12
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Local .python-version exists and has the right version.
|
||||||
|
let local_python_version = context.read(PYTHON_VERSION_FILENAME);
|
||||||
|
assert_snapshot!(local_python_version, @r###"
|
||||||
|
3.11
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Global .python-version still exists and has the right version.
|
||||||
|
let global_python_version = context.read(&global_version_path);
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
assert_snapshot!(global_python_version, @r###"
|
||||||
|
3.12
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// We do not need a Python interpreter to pin without `--resolved`
|
/// 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)
|
/// (skip on Windows because the snapshot is different and the behavior is not platform dependent)
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
|
|
@ -2387,7 +2387,6 @@ fn resolve_top_level() -> anyhow::Result<()> {
|
||||||
ignore = "Configuration tests are not yet supported on Windows"
|
ignore = "Configuration tests are not yet supported on Windows"
|
||||||
)]
|
)]
|
||||||
fn resolve_user_configuration() -> anyhow::Result<()> {
|
fn resolve_user_configuration() -> anyhow::Result<()> {
|
||||||
// Create a temporary directory to store the user configuration.
|
|
||||||
let xdg = assert_fs::TempDir::new().expect("Failed to create temp dir");
|
let xdg = assert_fs::TempDir::new().expect("Failed to create temp dir");
|
||||||
let uv = xdg.child("uv");
|
let uv = xdg.child("uv");
|
||||||
let config = uv.child("uv.toml");
|
let config = uv.child("uv.toml");
|
||||||
|
@ -3618,6 +3617,7 @@ fn invalid_conflicts() -> anyhow::Result<()> {
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_conflicts() -> anyhow::Result<()> {
|
fn valid_conflicts() -> anyhow::Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
let xdg = assert_fs::TempDir::new().expect("Failed to create temp dir");
|
||||||
let pyproject = context.temp_dir.child("pyproject.toml");
|
let pyproject = context.temp_dir.child("pyproject.toml");
|
||||||
|
|
||||||
// Write in `pyproject.toml` schema.
|
// Write in `pyproject.toml` schema.
|
||||||
|
@ -3632,7 +3632,8 @@ fn valid_conflicts() -> anyhow::Result<()> {
|
||||||
[{extra = "x1"}, {extra = "x2"}],
|
[{extra = "x1"}, {extra = "x2"}],
|
||||||
]
|
]
|
||||||
"#})?;
|
"#})?;
|
||||||
uv_snapshot!(context.filters(), add_shared_args(context.lock(), context.temp_dir.path()), @r###"
|
uv_snapshot!(context.filters(), add_shared_args(context.lock(), context.temp_dir.path())
|
||||||
|
.env("XDG_CONFIG_HOME", xdg.path()), @r###"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
|
@ -175,6 +175,111 @@ fn tool_install() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_install_with_global_python() -> Result<()> {
|
||||||
|
let context = TestContext::new_with_versions(&["3.11", "3.12"])
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_exe_suffix();
|
||||||
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
let uv = context.user_config_dir.child("uv");
|
||||||
|
let versions = uv.child(".python-version");
|
||||||
|
versions.write_str("3.11")?;
|
||||||
|
|
||||||
|
// Install a tool
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("flask")
|
||||||
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||||
|
.env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ blinker==1.7.0
|
||||||
|
+ click==8.1.7
|
||||||
|
+ flask==3.0.2
|
||||||
|
+ itsdangerous==2.1.2
|
||||||
|
+ jinja2==3.1.3
|
||||||
|
+ markupsafe==2.1.5
|
||||||
|
+ werkzeug==3.0.1
|
||||||
|
Installed 1 executable: flask
|
||||||
|
"###);
|
||||||
|
|
||||||
|
tool_dir.child("flask").assert(predicate::path::is_dir());
|
||||||
|
assert!(bin_dir
|
||||||
|
.child(format!("flask{}", std::env::consts::EXE_SUFFIX))
|
||||||
|
.exists());
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.11.[X]
|
||||||
|
Flask 3.0.2
|
||||||
|
Werkzeug 3.0.1
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Change global version
|
||||||
|
uv_snapshot!(context.filters(), context.python_pin().arg("3.12").arg("--global"),
|
||||||
|
@r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Updated `[UV_USER_CONFIG_DIR]/.python-version` from `3.11` -> `3.12`
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Install flask again
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("flask")
|
||||||
|
.arg("--reinstall")
|
||||||
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||||
|
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Uninstalled [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
~ blinker==1.7.0
|
||||||
|
~ click==8.1.7
|
||||||
|
~ flask==3.0.2
|
||||||
|
~ itsdangerous==2.1.2
|
||||||
|
~ jinja2==3.1.3
|
||||||
|
~ markupsafe==2.1.5
|
||||||
|
~ werkzeug==3.0.1
|
||||||
|
Installed 1 executable: flask
|
||||||
|
");
|
||||||
|
|
||||||
|
// Currently, when reinstalling a tool we use the original version the tool
|
||||||
|
// was installed with, not the most up-to-date global version
|
||||||
|
uv_snapshot!(context.filters(), Command::new("flask").arg("--version").env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
Python 3.11.[X]
|
||||||
|
Flask 3.0.2
|
||||||
|
Werkzeug 3.0.1
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_with_editable() -> Result<()> {
|
fn tool_install_with_editable() -> Result<()> {
|
||||||
let context = TestContext::new("3.12")
|
let context = TestContext::new("3.12")
|
||||||
|
@ -1471,7 +1576,7 @@ fn tool_install_uninstallable() {
|
||||||
.arg("pyenv")
|
.arg("pyenv")
|
||||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
|
||||||
.env(EnvVars::PATH, bin_dir.as_os_str()), @r###"
|
.env(EnvVars::PATH, bin_dir.as_os_str()), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
@ -1499,7 +1604,7 @@ fn tool_install_uninstallable() {
|
||||||
|
|
||||||
|
|
||||||
hint: This usually indicates a problem with the package or the build environment.
|
hint: This usually indicates a problem with the package or the build environment.
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Ensure the tool environment is not created.
|
// Ensure the tool environment is not created.
|
||||||
tool_dir.child("pyenv").assert(predicate::path::missing());
|
tool_dir.child("pyenv").assert(predicate::path::missing());
|
||||||
|
|
|
@ -52,16 +52,20 @@ This behavior can be
|
||||||
### Python version files
|
### Python version files
|
||||||
|
|
||||||
The `.python-version` file can be used to create a default Python version request. uv searches for a
|
The `.python-version` file can be used to create a default Python version request. uv searches for a
|
||||||
`.python-version` file in the working directory and each of its parents. Any of the request formats
|
`.python-version` file in the working directory and each of its parents. If none is found, uv will
|
||||||
described above can be used, though use of a version number is recommended for interoperability with
|
check the user-level configuration directory. Any of the request formats described above can be
|
||||||
other tools.
|
used, though use of a version number is recommended for interoperability with other tools.
|
||||||
|
|
||||||
A `.python-version` file can be created in the current directory with the
|
A `.python-version` file can be created in the current directory with the
|
||||||
[`uv python pin`](../reference/cli.md/#uv-python-pin) command.
|
[`uv python pin`](../reference/cli.md/#uv-python-pin) command.
|
||||||
|
|
||||||
|
A global `.python-version` file can be created in the user configuration directory with the
|
||||||
|
[`uv python pin --global`](../reference/cli.md/#uv-python-pin) command.
|
||||||
|
|
||||||
Discovery of `.python-version` files can be disabled with `--no-config`.
|
Discovery of `.python-version` files can be disabled with `--no-config`.
|
||||||
|
|
||||||
uv will not search for `.python-version` files beyond project or workspace boundaries.
|
uv will not search for `.python-version` files beyond project or workspace boundaries (with the
|
||||||
|
exception of the user configuration directory).
|
||||||
|
|
||||||
## Installing a Python version
|
## Installing a Python version
|
||||||
|
|
||||||
|
|
|
@ -407,6 +407,10 @@ Used for trusted publishing via `uv publish`. Contains the oidc token url.
|
||||||
|
|
||||||
General proxy for all network requests.
|
General proxy for all network requests.
|
||||||
|
|
||||||
|
### `APPDATA`
|
||||||
|
|
||||||
|
Path to user-level configuration directory on Windows systems.
|
||||||
|
|
||||||
### `BASH_VERSION`
|
### `BASH_VERSION`
|
||||||
|
|
||||||
Used to detect Bash shell usage.
|
Used to detect Bash shell usage.
|
||||||
|
@ -567,6 +571,10 @@ Path to system-level configuration directory on Windows systems.
|
||||||
|
|
||||||
Use to create the tracing durations file via the `tracing-durations-export` feature.
|
Use to create the tracing durations file via the `tracing-durations-export` feature.
|
||||||
|
|
||||||
|
### `USERPROFILE`
|
||||||
|
|
||||||
|
Path to root directory of user's profile on Windows systems.
|
||||||
|
|
||||||
### `UV`
|
### `UV`
|
||||||
|
|
||||||
The path to the binary that was used to invoke uv.
|
The path to the binary that was used to invoke uv.
|
||||||
|
|
|
@ -5186,6 +5186,14 @@ uv python pin [OPTIONS] [REQUEST]
|
||||||
|
|
||||||
<p>See <code>--project</code> to only change the project root directory.</p>
|
<p>See <code>--project</code> to only change the project root directory.</p>
|
||||||
|
|
||||||
|
</dd><dt id="uv-python-pin--global"><a href="#uv-python-pin--global"><code>--global</code></a></dt><dd><p>Update the global Python version pin.</p>
|
||||||
|
|
||||||
|
<p>Writes the pinned Python version to a <code>.python-version</code> file in the uv user configuration directory: <code>XDG_CONFIG_HOME/uv</code> on Linux/macOS and <code>%APPDATA%/uv</code> on Windows.</p>
|
||||||
|
|
||||||
|
<p>When a local Python version pin is not found in the working directory or an ancestor directory, this version will be used instead.</p>
|
||||||
|
|
||||||
|
<p>Unlike local version pins, this version is used as the default for commands that mutate global state, like <code>uv tool install</code>.</p>
|
||||||
|
|
||||||
</dd><dt id="uv-python-pin--help"><a href="#uv-python-pin--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
|
</dd><dt id="uv-python-pin--help"><a href="#uv-python-pin--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
|
||||||
|
|
||||||
</dd><dt id="uv-python-pin--native-tls"><a href="#uv-python-pin--native-tls"><code>--native-tls</code></a></dt><dd><p>Whether to load TLS certificates from the platform’s native certificate store.</p>
|
</dd><dt id="uv-python-pin--native-tls"><a href="#uv-python-pin--native-tls"><code>--native-tls</code></a></dt><dd><p>Whether to load TLS certificates from the platform’s native certificate store.</p>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue