From 99d278f9f5a376a3928c3a96a18b41f47f31c420 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 22 Aug 2024 18:07:30 -0500 Subject: [PATCH] Treat `.pyw` files as scripts in `uv run` on Windows (#6453) Closes https://github.com/astral-sh/uv/issues/6435 --- crates/uv/src/commands/project/run.rs | 36 ++++++++++++++++++++++++ crates/uv/tests/run.rs | 40 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 7890252199d7..269dc598adbe 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -722,6 +722,8 @@ enum RunCommand { Python(Vec), /// Execute a `python` script. PythonScript(PathBuf, Vec), + /// Execute a `pythonw` script (Windows only). + PythonGuiScript(PathBuf, Vec), /// Execute an external command. External(OsString, Vec), /// Execute an empty command (in practice, `python` with no arguments). @@ -734,6 +736,7 @@ impl RunCommand { match self { Self::Python(_) => Cow::Borrowed("python"), Self::PythonScript(_, _) | Self::Empty => Cow::Borrowed("python"), + Self::PythonGuiScript(_, _) => Cow::Borrowed("pythonw"), Self::External(executable, _) => executable.to_string_lossy(), } } @@ -752,6 +755,25 @@ impl RunCommand { process.args(args); process } + Self::PythonGuiScript(target, args) => { + let python_executable = interpreter.sys_executable(); + + // Use `pythonw.exe` if it exists, otherwise fall back to `python.exe`. + // See `install-wheel-rs::get_script_executable`.gd + let pythonw_executable = python_executable + .file_name() + .map(|name| { + let new_name = name.to_string_lossy().replace("python", "pythonw"); + python_executable.with_file_name(new_name) + }) + .filter(|path| path.is_file()) + .unwrap_or_else(|| python_executable.to_path_buf()); + + let mut process = Command::new(&pythonw_executable); + process.arg(target); + process.args(args); + process + } Self::External(executable, args) => { let mut process = Command::new(executable); process.args(args); @@ -779,6 +801,13 @@ impl std::fmt::Display for RunCommand { } Ok(()) } + Self::PythonGuiScript(target, args) => { + write!(f, "pythonw {}", target.display())?; + for arg in args { + write!(f, " {}", arg.to_string_lossy())?; + } + Ok(()) + } Self::External(executable, args) => { write!(f, "{}", executable.to_string_lossy())?; for arg in args { @@ -811,6 +840,13 @@ impl From<&ExternalCommand> for RunCommand { && target_path.exists() { Self::PythonScript(target_path, args.to_vec()) + } else if cfg!(windows) + && target_path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("pyw")) + && target_path.exists() + { + Self::PythonGuiScript(target_path, args.to_vec()) } else { Self::External( target.clone(), diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index eb03005c031b..713fc482328c 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -356,6 +356,46 @@ fn run_pep723_script() -> Result<()> { Ok(()) } +/// Run a `.pyw` script. The script should be executed with `pythonw.exe`. +#[test] +#[cfg(windows)] +fn run_pythonw_script() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio"] + "# + })?; + + let test_script = context.temp_dir.child("main.pyw"); + test_script.write_str(indoc! { r" + import anyio + " + })?; + + uv_snapshot!(context.filters(), context.run().arg("--preview").arg("main.pyw"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + foo==1.0.0 (from file://[TEMP_DIR]/) + + idna==3.6 + + sniffio==1.3.1 + "###); + + Ok(()) +} + /// Run a PEP 723-compatible script with `tool.uv` metadata. #[test] fn run_pep723_script_metadata() -> Result<()> {