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:
John Mumm 2025-03-13 13:48:37 +01:00 committed by GitHub
parent b4eabf9a61
commit 797f1fbac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 366 additions and 16 deletions

1
Cargo.lock generated
View file

@ -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",

View file

@ -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)]

View file

@ -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.

View file

@ -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.

View file

@ -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()

View file

@ -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";

View file

@ -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 }

View file

@ -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?;

View file

@ -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,
) )

View file

@ -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,
} }
} }
} }

View file

@ -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 {

View file

@ -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)]

View file

@ -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 -----

View file

@ -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());

View file

@ -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

View file

@ -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.

View file

@ -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&#8217;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&#8217;s native certificate store.</p>