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 <contact@zanie.dev>
This commit is contained in:
Rajko Radovanović
2024-12-10 12:35:17 -08:00
committed by GitHub
parent 761dafd0d1
commit f6f9179a5a
6 changed files with 85 additions and 2 deletions
+9 -2
View File
@@ -2702,7 +2702,7 @@ pub struct RunArgs {
/// Run a Python module.
///
/// Equivalent to `python -m <module>`.
#[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,
+7
View File
@@ -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();
+2
View File
@@ -141,6 +141,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
command: Some(command),
module,
script,
gui_script,
..
}) = &mut **command
{
@@ -150,6 +151,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
command,
*module,
*script,
*gui_script,
settings.connectivity,
settings.native_tls,
&settings.allow_insecure_host,
+1
View File
@@ -296,6 +296,7 @@ impl RunSettings {
only_dev,
no_editable,
script: _,
gui_script: _,
command: _,
with,
with_editable,
+62
View File
@@ -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();
+4
View File
@@ -184,6 +184,10 @@ uv run [OPTIONS] [COMMAND]
<p>May be provided multiple times.</p>
</dd><dt><code>--gui-script</code></dt><dd><p>Run the given path as a Python GUI script.</p>
<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>
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
</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>