Require --global for removal of the global Python pin (#14169)

While reviewing https://github.com/astral-sh/uv/pull/14107, @oconnor663
pointed out a bug where we allow `uv python pin --rm` to delete the
global pin without the `--global` flag. I think that shouldn't be
allowed? I'm not 100% certain though.
This commit is contained in:
Zanie Blue 2025-06-26 12:22:38 -05:00
parent e40357e529
commit 7ea7071939
5 changed files with 62 additions and 11 deletions

1
Cargo.lock generated
View file

@ -4665,7 +4665,6 @@ 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

@ -217,6 +217,19 @@ impl PythonVersionFile {
} }
} }
/// Create a new representation of a global Python version file.
///
/// Returns [`None`] if the user configuration directory cannot be determined.
pub fn global() -> Option<Self> {
let path = user_uv_config_dir()?.join(PYTHON_VERSION_FILENAME);
Some(Self::new(path))
}
/// Returns `true` if the version file is a global version file.
pub fn is_global(&self) -> bool {
PythonVersionFile::global().is_some_and(|global| self.path() == global.path())
}
/// Return the first request declared in the file, if any. /// Return the first request declared in the file, if any.
pub fn version(&self) -> Option<&PythonRequest> { pub fn version(&self) -> Option<&PythonRequest> {
self.versions.first() self.versions.first()
@ -260,6 +273,9 @@ impl PythonVersionFile {
/// Update the version file on the file system. /// Update the version file on the file system.
pub async fn write(&self) -> Result<(), std::io::Error> { pub async fn write(&self) -> Result<(), std::io::Error> {
debug!("Writing Python versions to `{}`", self.path.display()); debug!("Writing Python versions to `{}`", self.path.display());
if let Some(parent) = self.path.parent() {
fs_err::tokio::create_dir_all(parent).await?;
}
fs::tokio::write( fs::tokio::write(
&self.path, &self.path,
self.versions self.versions

View file

@ -24,7 +24,6 @@ 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

@ -9,7 +9,6 @@ use tracing::debug;
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::BaseClientBuilder; use uv_client::BaseClientBuilder;
use uv_configuration::{DependencyGroupsWithDefaults, PreviewMode}; use uv_configuration::{DependencyGroupsWithDefaults, PreviewMode};
use uv_dirs::user_uv_config_dir;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_python::{ use uv_python::{
EnvironmentPreference, PYTHON_VERSION_FILENAME, PythonDownloads, PythonInstallation, EnvironmentPreference, PYTHON_VERSION_FILENAME, PythonDownloads, PythonInstallation,
@ -72,10 +71,20 @@ pub(crate) async fn pin(
} }
bail!("No Python version file found"); bail!("No Python version file found");
}; };
if !global && file.is_global() {
bail!("No Python version file found; use `--rm --global` to remove the global pin");
}
fs_err::tokio::remove_file(file.path()).await?; fs_err::tokio::remove_file(file.path()).await?;
writeln!( writeln!(
printer.stdout(), printer.stdout(),
"Removed Python version file at `{}`", "Removed {} at `{}`",
if global {
"global Python pin"
} else {
"Python version file"
},
file.path().user_display() file.path().user_display()
)?; )?;
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
@ -192,12 +201,11 @@ 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 = if global { let new = if global {
let Some(config_dir) = user_uv_config_dir() else { let Some(new) = PythonVersionFile::global() else {
return Err(anyhow::anyhow!("No user-level config directory found.")); // TODO(zanieb): We should find a nice way to surface that as an error
bail!("Failed to determine directory for global Python pin");
}; };
fs_err::tokio::create_dir_all(&config_dir).await?; new.with_versions(vec![request])
PythonVersionFile::new(config_dir.join(PYTHON_VERSION_FILENAME))
.with_versions(vec![request])
} else { } else {
PythonVersionFile::new(project_dir.join(PYTHON_VERSION_FILENAME)) PythonVersionFile::new(project_dir.join(PYTHON_VERSION_FILENAME))
.with_versions(vec![request]) .with_versions(vec![request])

View file

@ -855,7 +855,7 @@ fn python_pin_rm() {
error: No Python version file found error: No Python version file found
"); ");
// Remove the local pin // Create and remove a local pin
context.python_pin().arg("3.12").assert().success(); context.python_pin().arg("3.12").assert().success();
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @r" uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @r"
success: true success: true
@ -892,12 +892,41 @@ fn python_pin_rm() {
.arg("--global") .arg("--global")
.assert() .assert()
.success(); .success();
uv_snapshot!(context.filters(), context.python_pin().arg("--rm").arg("--global"), @r" uv_snapshot!(context.filters(), context.python_pin().arg("--rm").arg("--global"), @r"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
Removed Python version file at `[UV_USER_CONFIG_DIR]/.python-version` Removed global Python pin at `[UV_USER_CONFIG_DIR]/.python-version`
----- stderr ----- ----- stderr -----
"); ");
// Add the global pin again
context
.python_pin()
.arg("3.12")
.arg("--global")
.assert()
.success();
// Remove the local pin
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @r"
success: true
exit_code: 0
----- stdout -----
Removed Python version file at `.python-version`
----- stderr -----
");
// The global pin should not be removed without `--global`
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No Python version file found; use `--rm --global` to remove the global pin
");
} }