diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 84fc59e9b..da4524aaa 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -5032,6 +5032,10 @@ pub struct PythonPinArgs { /// directory, this version will be used instead. #[arg(long)] pub global: bool, + + /// Remove the Python version pin. + #[arg(long, conflicts_with = "request", conflicts_with = "resolved")] + pub rm: bool, } #[derive(Args)] diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index 40dbfa1d9..5e2b89c18 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -20,6 +20,7 @@ use crate::commands::{ExitStatus, project::find_requires_python}; use crate::printer::Printer; /// Pin to a specific Python version. +#[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn pin( project_dir: &Path, request: Option, @@ -27,6 +28,7 @@ pub(crate) async fn pin( python_preference: PythonPreference, no_project: bool, global: bool, + rm: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -56,6 +58,19 @@ pub(crate) async fn pin( PythonVersionFile::discover(project_dir, &VersionFileDiscoveryOptions::default()).await }; + if rm { + let Some(file) = version_file? else { + bail!("No Python version file found"); + }; + fs_err::tokio::remove_file(file.path()).await?; + writeln!( + printer.stdout(), + "Removed Python version file at `{}`", + file.path().user_display() + )?; + return Ok(ExitStatus::Success); + } + let Some(request) = request else { // Display the current pinned Python version if let Some(file) = version_file? { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index a0f543a0c..1d4aa932c 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1466,6 +1466,7 @@ async fn run(mut cli: Cli) -> Result { globals.python_preference, args.no_project, args.global, + args.rm, &cache, printer, ) diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 377756dbb..38d3d16fb 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1056,6 +1056,7 @@ pub(crate) struct PythonPinSettings { pub(crate) resolved: bool, pub(crate) no_project: bool, pub(crate) global: bool, + pub(crate) rm: bool, } impl PythonPinSettings { @@ -1068,6 +1069,7 @@ impl PythonPinSettings { resolved, no_project, global, + rm, } = args; Self { @@ -1075,6 +1077,7 @@ impl PythonPinSettings { resolved: flag(resolved, no_resolved).unwrap_or(false), no_project, global, + rm, } } } diff --git a/crates/uv/tests/it/python_pin.rs b/crates/uv/tests/it/python_pin.rs index d6c267c7a..6aff28dbf 100644 --- a/crates/uv/tests/it/python_pin.rs +++ b/crates/uv/tests/it/python_pin.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use crate::common::{TestContext, uv_snapshot}; use anyhow::Result; +use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir}; use insta::assert_snapshot; use uv_python::{ @@ -814,3 +815,63 @@ fn python_pin_with_comments() -> Result<()> { Ok(()) } + +#[test] +fn python_pin_rm() { + let context: TestContext = TestContext::new_with_versions(&["3.12"]); + + uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No Python version file found + "); + + // Remove the local pin + context.python_pin().arg("3.12").assert().success(); + uv_snapshot!(context.filters(), context.python_pin().arg("--rm"), @r" + 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"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No Python version file found + "); + + // Global does not detect the local pin + context.python_pin().arg("3.12").assert().success(); + uv_snapshot!(context.filters(), context.python_pin().arg("--rm").arg("--global"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: No Python version file found + "); + + context + .python_pin() + .arg("3.12") + .arg("--global") + .assert() + .success(); + uv_snapshot!(context.filters(), context.python_pin().arg("--rm").arg("--global"), @r" + success: true + exit_code: 0 + ----- stdout ----- + Removed Python version file at `[UV_USER_CONFIG_DIR]/.python-version` + + ----- stderr ----- + "); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index b7e41728a..d89d4498e 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2904,6 +2904,7 @@ uv python pin [OPTIONS] [REQUEST]
--resolved

Write the resolved Python interpreter path instead of the request.

Ensures that the exact same interpreter is used.

This option is usually not safe to use when committing the .python-version file to version control.

+
--rm

Remove the Python version pin

--verbose, -v

Use verbose output.

You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)