From f6f9179a5a605e923bc25de60e4fc036f8d8bc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rajko=20Radovanovi=C4=87?= <57970926+rajko-rad@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:35:17 -0800 Subject: [PATCH] =?UTF-8?q?Add=20--gui-script=20flag=20for=20running=20Pyt?= =?UTF-8?q?hon=20scripts=20with=20pythonw.exe=20on=20=E2=80=A6=20(#9152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses #6805 ## Summary This PR adds a `--gui-script` flag to `uv run` that allows running Python scripts with `pythonw.exe` on Windows, regardless of file extension. This solves the issue where users need to maintain duplicate `.py` and `.pyw` files to run the same script with and without a console window. The implementation follows the pattern established by the existing `--script` flag, but uses `pythonw.exe` instead of `python.exe` on Windows. On non-Windows platforms, the flag is present but returns an error indicating it's Windows-only functionality. Changes: - Added `--gui-script` flag (Windows-only) - Added Windows test to verify GUI script behavior - Added non-Windows test to verify proper error message - Updated CLI documentation ## Test Plan The changes are tested through: 1. New Windows-specific test that verifies: - Script runs successfully with `pythonw.exe` when using `--gui-script` - Console output is suppressed in GUI mode but visible in regular mode - Same script can be run both ways without modification 2. New non-Windows test that verifies: - Appropriate error message when `--gui-script` is used on non-Windows platforms 3. Documentation updates to clearly indicate Windows-only functionality --------- Co-authored-by: Zanie Blue --- crates/uv-cli/src/lib.rs | 11 ++++- crates/uv/src/commands/project/run.rs | 7 +++ crates/uv/src/lib.rs | 2 + crates/uv/src/settings.rs | 1 + crates/uv/tests/it/run.rs | 62 +++++++++++++++++++++++++++ docs/reference/cli.md | 4 ++ 6 files changed, 85 insertions(+), 2 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 8324d5f87..610717bee 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 165709c11..2ac15cc25 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 f6ffefe8a..c3a76abd5 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 cd8d70593..6c6535d26 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 5f758bb4e..4f20d6c3a 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 6878535f4..eda25f29a 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.