Skip to content

Commit 83ec9e3

Browse files
authored
Require --global for removal of the global Python pin (#14169)
While reviewing #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.
1 parent 8644178 commit 83ec9e3

5 files changed

Lines changed: 62 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-python/src/version_files.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,19 @@ impl PythonVersionFile {
217217
}
218218
}
219219

220+
/// Create a new representation of a global Python version file.
221+
///
222+
/// Returns [`None`] if the user configuration directory cannot be determined.
223+
pub fn global() -> Option<Self> {
224+
let path = user_uv_config_dir()?.join(PYTHON_VERSION_FILENAME);
225+
Some(Self::new(path))
226+
}
227+
228+
/// Returns `true` if the version file is a global version file.
229+
pub fn is_global(&self) -> bool {
230+
PythonVersionFile::global().is_some_and(|global| self.path() == global.path())
231+
}
232+
220233
/// Return the first request declared in the file, if any.
221234
pub fn version(&self) -> Option<&PythonRequest> {
222235
self.versions.first()
@@ -260,6 +273,9 @@ impl PythonVersionFile {
260273
/// Update the version file on the file system.
261274
pub async fn write(&self) -> Result<(), std::io::Error> {
262275
debug!("Writing Python versions to `{}`", self.path.display());
276+
if let Some(parent) = self.path.parent() {
277+
fs_err::tokio::create_dir_all(parent).await?;
278+
}
263279
fs::tokio::write(
264280
&self.path,
265281
self.versions

crates/uv/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ uv-cli = { workspace = true }
2424
uv-client = { workspace = true }
2525
uv-configuration = { workspace = true }
2626
uv-console = { workspace = true }
27-
uv-dirs = { workspace = true }
2827
uv-dispatch = { workspace = true }
2928
uv-distribution = { workspace = true }
3029
uv-distribution-filename = { workspace = true }

crates/uv/src/commands/python/pin.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use tracing::debug;
99
use uv_cache::Cache;
1010
use uv_client::BaseClientBuilder;
1111
use uv_configuration::{DependencyGroupsWithDefaults, PreviewMode};
12-
use uv_dirs::user_uv_config_dir;
1312
use uv_fs::Simplified;
1413
use uv_python::{
1514
EnvironmentPreference, PYTHON_VERSION_FILENAME, PythonDownloads, PythonInstallation,
@@ -72,10 +71,20 @@ pub(crate) async fn pin(
7271
}
7372
bail!("No Python version file found");
7473
};
74+
75+
if !global && file.is_global() {
76+
bail!("No Python version file found; use `--rm --global` to remove the global pin");
77+
}
78+
7579
fs_err::tokio::remove_file(file.path()).await?;
7680
writeln!(
7781
printer.stdout(),
78-
"Removed Python version file at `{}`",
82+
"Removed {} at `{}`",
83+
if global {
84+
"global Python pin"
85+
} else {
86+
"Python version file"
87+
},
7988
file.path().user_display()
8089
)?;
8190
return Ok(ExitStatus::Success);
@@ -192,12 +201,11 @@ pub(crate) async fn pin(
192201
let existing = version_file.ok().flatten();
193202
// TODO(zanieb): Allow updating the discovered version file with an `--update` flag.
194203
let new = if global {
195-
let Some(config_dir) = user_uv_config_dir() else {
196-
return Err(anyhow::anyhow!("No user-level config directory found."));
204+
let Some(new) = PythonVersionFile::global() else {
205+
// TODO(zanieb): We should find a nice way to surface that as an error
206+
bail!("Failed to determine directory for global Python pin");
197207
};
198-
fs_err::tokio::create_dir_all(&config_dir).await?;
199-
PythonVersionFile::new(config_dir.join(PYTHON_VERSION_FILENAME))
200-
.with_versions(vec![request])
208+
new.with_versions(vec![request])
201209
} else {
202210
PythonVersionFile::new(project_dir.join(PYTHON_VERSION_FILENAME))
203211
.with_versions(vec![request])

crates/uv/tests/it/python_pin.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,7 @@ fn python_pin_rm() {
855855
error: No Python version file found
856856
");
857857

858-
// Remove the local pin
858+
// Create and remove a local pin
859859
context.python_pin().arg("3.12").assert().success();
860860
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @r"
861861
success: true
@@ -892,12 +892,41 @@ fn python_pin_rm() {
892892
.arg("--global")
893893
.assert()
894894
.success();
895+
895896
uv_snapshot!(context.filters(), context.python_pin().arg("--rm").arg("--global"), @r"
896897
success: true
897898
exit_code: 0
898899
----- stdout -----
899-
Removed Python version file at `[UV_USER_CONFIG_DIR]/.python-version`
900+
Removed global Python pin at `[UV_USER_CONFIG_DIR]/.python-version`
901+
902+
----- stderr -----
903+
");
904+
905+
// Add the global pin again
906+
context
907+
.python_pin()
908+
.arg("3.12")
909+
.arg("--global")
910+
.assert()
911+
.success();
912+
913+
// Remove the local pin
914+
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @r"
915+
success: true
916+
exit_code: 0
917+
----- stdout -----
918+
Removed Python version file at `.python-version`
919+
920+
----- stderr -----
921+
");
922+
923+
// The global pin should not be removed without `--global`
924+
uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @r"
925+
success: false
926+
exit_code: 2
927+
----- stdout -----
900928
901929
----- stderr -----
930+
error: No Python version file found; use `--rm --global` to remove the global pin
902931
");
903932
}

0 commit comments

Comments
 (0)