diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 8324d5f8701b..610717bee953 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2702,7 +2702,7 @@ pub struct RunArgs { /// Run a Python module. /// /// Equivalent to `python -m `. - #[arg(short, long, conflicts_with = "script")] + #[arg(short, long, conflicts_with_all = ["script", "gui_script"])] pub module: bool, /// Only include the development dependency group. @@ -2801,9 +2801,16 @@ pub struct RunArgs { /// /// Using `--script` will attempt to parse the path as a PEP 723 script, /// irrespective of its extension. - #[arg(long, short, conflicts_with = "module")] + #[arg(long, short, conflicts_with_all = ["module", "gui_script"])] pub script: bool, + /// Run the given path as a Python GUI script. + /// + /// Using `--gui-script` will attempt to parse the path as a PEP 723 script and run it with pythonw.exe, + /// irrespective of its extension. Only available on Windows. + #[arg(long, conflicts_with_all = ["script", "module"])] + pub gui_script: bool, + #[command(flatten)] pub installer: ResolverInstallerArgs, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 165709c11116..2ac15cc25c5f 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -1344,10 +1344,12 @@ impl std::fmt::Display for RunCommand { impl RunCommand { /// Determine the [`RunCommand`] for a given set of arguments. + #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn from_args( command: &ExternalCommand, module: bool, script: bool, + gui_script: bool, connectivity: Connectivity, native_tls: bool, allow_insecure_host: &[TrustedHost], @@ -1401,6 +1403,11 @@ impl RunCommand { return Ok(Self::PythonModule(target.clone(), args.to_vec())); } else if script { return Ok(Self::PythonScript(target.clone().into(), args.to_vec())); + } else if gui_script { + if cfg!(windows) { + return Ok(Self::PythonGuiScript(target.clone().into(), args.to_vec())); + } + anyhow::bail!("`--gui-script` is only supported on Windows. Did you mean `--script`?"); } let metadata = target_path.metadata(); diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index f6ffefe8ac6c..c3a76abd5950 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -141,6 +141,7 @@ async fn run(mut cli: Cli) -> Result { command: Some(command), module, script, + gui_script, .. }) = &mut **command { @@ -150,6 +151,7 @@ async fn run(mut cli: Cli) -> Result { command, *module, *script, + *gui_script, settings.connectivity, settings.native_tls, &settings.allow_insecure_host, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index cd8d705930ac..6c6535d26f9d 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -296,6 +296,7 @@ impl RunSettings { only_dev, no_editable, script: _, + gui_script: _, command: _, with, with_editable, diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 5f758bb4e13f..4f20d6c3af58 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -2893,6 +2893,68 @@ fn run_script_explicit_directory() -> Result<()> { Ok(()) } +#[test] +#[cfg(windows)] +fn run_gui_script_explicit() -> Result<()> { + let context = TestContext::new("3.12"); + + let test_script = context.temp_dir.child("script"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [] + # /// + import sys + import os + + executable = os.path.basename(sys.executable).lower() + if not executable.startswith("pythonw"): + print(f"Error: Expected pythonw.exe but got: {executable}", file=sys.stderr) + sys.exit(1) + + print(f"Using executable: {executable}", file=sys.stderr) + "#})?; + + uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("script"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Reading inline script metadata from `script` + Resolved in [TIME] + Audited in [TIME] + Using executable: pythonw.exe + "###); + + Ok(()) +} + +#[test] +#[cfg(not(windows))] +fn run_gui_script_not_supported() -> Result<()> { + let context = TestContext::new("3.12"); + let test_script = context.temp_dir.child("script"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [] + # /// + print("Hello") + "#})?; + + uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("script"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: `--gui-script` is only supported on Windows. Did you mean `--script`? + "###); + + Ok(()) +} + #[test] fn run_remote_pep723_script() { let context = TestContext::new("3.12").with_filtered_python_names(); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 6878535f4aa8..eda25f29a74c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -184,6 +184,10 @@ uv run [OPTIONS] [COMMAND]

May be provided multiple times.

+
--gui-script

Run the given path as a Python GUI script.

+ +

Using --gui-script will attempt to parse the path as a PEP 723 script and run it with pythonw.exe, irrespective of its extension. Only available on Windows.

+
--help, -h

Display the concise help for this command

--index index

The URLs to use when resolving dependencies, in addition to the default index.