Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow --script to be provided with uv run - #10035

Merged
merged 1 commit into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,8 @@ pub(crate) enum RunCommand {
PythonZipapp(PathBuf, Vec<OsString>),
/// Execute a `python` script provided via `stdin`.
PythonStdin(Vec<u8>, Vec<OsString>),
/// Execute a `pythonw` script provided via `stdin`.
PythonGuiStdin(Vec<u8>, Vec<OsString>),
/// Execute a Python script provided via a remote URL.
PythonRemote(tempfile::NamedTempFile, Vec<OsString>),
/// Execute an external command.
Expand Down Expand Up @@ -1209,6 +1211,13 @@ impl RunCommand {
}
}
Self::PythonStdin(..) => Cow::Borrowed("python -c"),
Self::PythonGuiStdin(..) => {
if cfg!(windows) {
Cow::Borrowed("pythonw -c")
} else {
Cow::Borrowed("python -c")
}
}
Self::External(executable, _) => executable.to_string_lossy(),
}
}
Expand Down Expand Up @@ -1280,6 +1289,38 @@ impl RunCommand {

process
}
Self::PythonGuiStdin(script, 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("-c");

#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
process.arg(OsString::from_vec(script.clone()));
}

#[cfg(not(unix))]
{
let script = String::from_utf8(script.clone()).expect("script is valid UTF-8");
process.arg(script);
}
process.args(args);

process
}
Self::External(executable, args) => {
let mut process = Command::new(executable);
process.args(args);
Expand Down Expand Up @@ -1328,6 +1369,10 @@ impl std::fmt::Display for RunCommand {
write!(f, "python -c")?;
Ok(())
}
Self::PythonGuiStdin(..) => {
write!(f, "pythonw -c")?;
Ok(())
}
Self::External(executable, args) => {
write!(f, "{}", executable.to_string_lossy())?;
for arg in args {
Expand Down Expand Up @@ -1360,6 +1405,19 @@ impl RunCommand {
return Ok(Self::Empty);
};

if target.eq_ignore_ascii_case("-") {
let mut buf = Vec::with_capacity(1024);
std::io::stdin().read_to_end(&mut buf)?;

return if module {
Err(anyhow!("Cannot run a Python module from stdin"))
} else if gui_script {
Ok(Self::PythonGuiStdin(buf, args.to_vec()))
} else {
Ok(Self::PythonStdin(buf, args.to_vec()))
};
}

let target_path = PathBuf::from(target);

// Determine whether the user provided a remote script.
Expand Down Expand Up @@ -1402,21 +1460,17 @@ impl RunCommand {

if module {
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 {
return Ok(Self::PythonGuiScript(target.clone().into(), args.to_vec()));
} else if script {
return Ok(Self::PythonScript(target.clone().into(), args.to_vec()));
}

let metadata = target_path.metadata();
let is_file = metadata.as_ref().map_or(false, std::fs::Metadata::is_file);
let is_dir = metadata.as_ref().map_or(false, std::fs::Metadata::is_dir);

if target.eq_ignore_ascii_case("-") {
let mut buf = Vec::with_capacity(1024);
std::io::stdin().read_to_end(&mut buf)?;
Ok(Self::PythonStdin(buf, args.to_vec()))
} else if target.eq_ignore_ascii_case("python") {
if target.eq_ignore_ascii_case("python") {
Ok(Self::Python(args.to_vec()))
} else if target_path
.extension()
Expand Down
6 changes: 3 additions & 3 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
Some(RunCommand::PythonRemote(script, _)) => {
Pep723Metadata::read(&script).await?.map(Pep723Item::Remote)
}
Some(RunCommand::PythonStdin(contents, _)) => {
Pep723Metadata::parse(contents)?.map(Pep723Item::Stdin)
}
Some(
RunCommand::PythonStdin(contents, _) | RunCommand::PythonGuiStdin(contents, _),
) => Pep723Metadata::parse(contents)?.map(Pep723Item::Stdin),
_ => None,
}
} else if let ProjectCommand::Remove(uv_cli::RemoveArgs {
Expand Down
118 changes: 118 additions & 0 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2544,6 +2544,20 @@ fn run_module() {
"#);
}

#[test]
fn run_module_stdin() {
let context = TestContext::new("3.12");

uv_snapshot!(context.filters(), context.run().arg("-m").arg("-"), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Cannot run a Python module from stdin
"###);
}

/// When the `pyproject.toml` file is invalid.
#[test]
fn run_project_toml_error() -> Result<()> {
Expand Down Expand Up @@ -2874,6 +2888,40 @@ fn run_script_explicit() -> Result<()> {
Ok(())
}

#[test]
fn run_script_explicit_stdin() -> 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 = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;

uv_snapshot!(context.filters(), context.run().arg("--script").arg("-").stdin(std::fs::File::open(test_script)?), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!

----- stderr -----
Reading inline script metadata from `stdin`
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);

Ok(())
}

#[test]
fn run_script_explicit_no_file() {
let context = TestContext::new("3.12");
Expand Down Expand Up @@ -2942,6 +2990,41 @@ fn run_gui_script_explicit_windows() -> Result<()> {
Ok(())
}

#[test]
#[cfg(windows)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--gui-script should just "work" on Unix too — so you can use a script on both platforms.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, if you're not going to assert which executable is used, we should combine the tests.

fn run_gui_script_explicit_stdin_windows() -> 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 = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;

uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("-").stdin(std::fs::File::open(test_script)?), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!

----- stderr -----
Reading inline script metadata from `stdin`
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);

Ok(())
}

#[test]
#[cfg(not(windows))]
fn run_gui_script_explicit_unix() -> Result<()> {
Expand Down Expand Up @@ -2974,6 +3057,41 @@ fn run_gui_script_explicit_unix() -> Result<()> {
Ok(())
}

#[test]
#[cfg(not(windows))]
fn run_gui_script_explicit_stdin_unix() -> 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 = [
# "iniconfig",
# ]
# ///
import iniconfig
print("Hello, world!")
"#
})?;

uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("-").stdin(std::fs::File::open(test_script)?), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!

----- stderr -----
Reading inline script metadata from `stdin`
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);

Ok(())
}

#[test]
fn run_remote_pep723_script() {
let context = TestContext::new("3.12").with_filtered_python_names();
Expand Down
Loading