Skip to content

Commit f6f9179

Browse files
rajko-radzanieb
andauthored
Add --gui-script flag for running Python scripts with pythonw.exe on … (#9152)
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 <[email protected]>
1 parent 761dafd commit f6f9179

File tree

6 files changed

+85
-2
lines changed

6 files changed

+85
-2
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2702,7 +2702,7 @@ pub struct RunArgs {
27022702
/// Run a Python module.
27032703
///
27042704
/// Equivalent to `python -m <module>`.
2705-
#[arg(short, long, conflicts_with = "script")]
2705+
#[arg(short, long, conflicts_with_all = ["script", "gui_script"])]
27062706
pub module: bool,
27072707

27082708
/// Only include the development dependency group.
@@ -2801,9 +2801,16 @@ pub struct RunArgs {
28012801
///
28022802
/// Using `--script` will attempt to parse the path as a PEP 723 script,
28032803
/// irrespective of its extension.
2804-
#[arg(long, short, conflicts_with = "module")]
2804+
#[arg(long, short, conflicts_with_all = ["module", "gui_script"])]
28052805
pub script: bool,
28062806

2807+
/// Run the given path as a Python GUI script.
2808+
///
2809+
/// Using `--gui-script` will attempt to parse the path as a PEP 723 script and run it with pythonw.exe,
2810+
/// irrespective of its extension. Only available on Windows.
2811+
#[arg(long, conflicts_with_all = ["script", "module"])]
2812+
pub gui_script: bool,
2813+
28072814
#[command(flatten)]
28082815
pub installer: ResolverInstallerArgs,
28092816

crates/uv/src/commands/project/run.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,10 +1344,12 @@ impl std::fmt::Display for RunCommand {
13441344

13451345
impl RunCommand {
13461346
/// Determine the [`RunCommand`] for a given set of arguments.
1347+
#[allow(clippy::fn_params_excessive_bools)]
13471348
pub(crate) async fn from_args(
13481349
command: &ExternalCommand,
13491350
module: bool,
13501351
script: bool,
1352+
gui_script: bool,
13511353
connectivity: Connectivity,
13521354
native_tls: bool,
13531355
allow_insecure_host: &[TrustedHost],
@@ -1401,6 +1403,11 @@ impl RunCommand {
14011403
return Ok(Self::PythonModule(target.clone(), args.to_vec()));
14021404
} else if script {
14031405
return Ok(Self::PythonScript(target.clone().into(), args.to_vec()));
1406+
} else if gui_script {
1407+
if cfg!(windows) {
1408+
return Ok(Self::PythonGuiScript(target.clone().into(), args.to_vec()));
1409+
}
1410+
anyhow::bail!("`--gui-script` is only supported on Windows. Did you mean `--script`?");
14041411
}
14051412

14061413
let metadata = target_path.metadata();

crates/uv/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
141141
command: Some(command),
142142
module,
143143
script,
144+
gui_script,
144145
..
145146
}) = &mut **command
146147
{
@@ -150,6 +151,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
150151
command,
151152
*module,
152153
*script,
154+
*gui_script,
153155
settings.connectivity,
154156
settings.native_tls,
155157
&settings.allow_insecure_host,

crates/uv/src/settings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ impl RunSettings {
296296
only_dev,
297297
no_editable,
298298
script: _,
299+
gui_script: _,
299300
command: _,
300301
with,
301302
with_editable,

crates/uv/tests/it/run.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,6 +2893,68 @@ fn run_script_explicit_directory() -> Result<()> {
28932893
Ok(())
28942894
}
28952895

2896+
#[test]
2897+
#[cfg(windows)]
2898+
fn run_gui_script_explicit() -> Result<()> {
2899+
let context = TestContext::new("3.12");
2900+
2901+
let test_script = context.temp_dir.child("script");
2902+
test_script.write_str(indoc! { r#"
2903+
# /// script
2904+
# requires-python = ">=3.11"
2905+
# dependencies = []
2906+
# ///
2907+
import sys
2908+
import os
2909+
2910+
executable = os.path.basename(sys.executable).lower()
2911+
if not executable.startswith("pythonw"):
2912+
print(f"Error: Expected pythonw.exe but got: {executable}", file=sys.stderr)
2913+
sys.exit(1)
2914+
2915+
print(f"Using executable: {executable}", file=sys.stderr)
2916+
"#})?;
2917+
2918+
uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("script"), @r###"
2919+
success: true
2920+
exit_code: 0
2921+
----- stdout -----
2922+
2923+
----- stderr -----
2924+
Reading inline script metadata from `script`
2925+
Resolved in [TIME]
2926+
Audited in [TIME]
2927+
Using executable: pythonw.exe
2928+
"###);
2929+
2930+
Ok(())
2931+
}
2932+
2933+
#[test]
2934+
#[cfg(not(windows))]
2935+
fn run_gui_script_not_supported() -> Result<()> {
2936+
let context = TestContext::new("3.12");
2937+
let test_script = context.temp_dir.child("script");
2938+
test_script.write_str(indoc! { r#"
2939+
# /// script
2940+
# requires-python = ">=3.11"
2941+
# dependencies = []
2942+
# ///
2943+
print("Hello")
2944+
"#})?;
2945+
2946+
uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("script"), @r###"
2947+
success: false
2948+
exit_code: 2
2949+
----- stdout -----
2950+
2951+
----- stderr -----
2952+
error: `--gui-script` is only supported on Windows. Did you mean `--script`?
2953+
"###);
2954+
2955+
Ok(())
2956+
}
2957+
28962958
#[test]
28972959
fn run_remote_pep723_script() {
28982960
let context = TestContext::new("3.12").with_filtered_python_names();

docs/reference/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ uv run [OPTIONS] [COMMAND]
184184

185185
<p>May be provided multiple times.</p>
186186

187+
</dd><dt><code>--gui-script</code></dt><dd><p>Run the given path as a Python GUI script.</p>
188+
189+
<p>Using <code>--gui-script</code> 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.</p>
190+
187191
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
188192

189193
</dd><dt><code>--index</code> <i>index</i></dt><dd><p>The URLs to use when resolving dependencies, in addition to the default index.</p>

0 commit comments

Comments
 (0)