Skip to content

Commit

Permalink
Add --gui-script flag for running Python scripts with pythonw.exe on … (
Browse files Browse the repository at this point in the history
#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]>
  • Loading branch information
rajko-rad and zanieb authored Dec 10, 2024
1 parent 761dafd commit f6f9179
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 2 deletions.
11 changes: 9 additions & 2 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,

Expand Down
7 changes: 7 additions & 0 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
command: Some(command),
module,
script,
gui_script,
..
}) = &mut **command
{
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ impl RunSettings {
only_dev,
no_editable,
script: _,
gui_script: _,
command: _,
with,
with_editable,
Expand Down
62 changes: 62 additions & 0 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit f6f9179

Please sign in to comment.