Skip to content

Commit

Permalink
Support uv run --script (#7739)
Browse files Browse the repository at this point in the history
This PR adds support for executing a script with ```uv run```, even when
the script does not have a ```.py``` extension. Addresses #7396.
  • Loading branch information
tfsingh authored Oct 2, 2024
1 parent b001483 commit 5ff7dc9
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 4 deletions.
9 changes: 8 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2472,7 +2472,7 @@ pub struct RunArgs {
/// Run a Python module.
///
/// Equivalent to `python -m <module>`.
#[arg(short, long)]
#[arg(short, long, conflicts_with = "script")]
pub module: bool,

/// Omit non-development dependencies.
Expand Down Expand Up @@ -2554,6 +2554,13 @@ pub struct RunArgs {
#[arg(long, conflicts_with = "locked")]
pub frozen: bool,

/// Run the given path as a Python script.
///
/// Using `--script` will attempt to parse the path as a PEP 723 script,
/// irrespective of its extension.
#[arg(long, short, conflicts_with = "module")]
pub script: bool,

#[command(flatten)]
pub installer: ResolverInstallerArgs,

Expand Down
8 changes: 7 additions & 1 deletion crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1021,14 +1021,20 @@ impl std::fmt::Display for RunCommand {
}

impl RunCommand {
pub(crate) fn from_args(command: &ExternalCommand, module: bool) -> anyhow::Result<Self> {
pub(crate) fn from_args(
command: &ExternalCommand,
module: bool,
script: bool,
) -> anyhow::Result<Self> {
let (target, args) = command.split();
let Some(target) = target else {
return Ok(Self::Empty);
};

if module {
return Ok(Self::PythonModule(target.clone(), args.to_vec()));
} else if script {
return Ok(Self::PythonScript(target.clone().into(), args.to_vec()));
}

let target_path = PathBuf::from(target);
Expand Down
7 changes: 5 additions & 2 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,13 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
// Parse the external command, if necessary.
let run_command = if let Commands::Project(command) = &*cli.command {
if let ProjectCommand::Run(uv_cli::RunArgs {
command, module, ..
command,
module,
script,
..
}) = &**command
{
Some(RunCommand::from_args(command, *module)?)
Some(RunCommand::from_args(command, *module, *script)?)
} else {
None
}
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 @@ -248,6 +248,7 @@ impl RunSettings {
module: _,
only_dev,
no_editable,
script: _,
command: _,
with,
with_editable,
Expand Down
67 changes: 67 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::{fixture::ChildPath, prelude::*};
use indoc::indoc;
use predicates::str::contains;
use std::path::Path;

use uv_python::PYTHON_VERSION_FILENAME;
Expand Down Expand Up @@ -2164,3 +2165,69 @@ fn run_script_without_build_system() -> Result<()> {

Ok(())
}

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

uv_snapshot!(context.filters(), context.run().arg("--script").arg("script"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
Reading inline script metadata from: script
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");
context
.run()
.arg("--script")
.arg("script")
.assert()
.stderr(contains("can't open file"))
.stderr(contains("[Errno 2] No such file or directory"));
}

#[cfg(target_family = "unix")]
#[test]
fn run_script_explicit_directory() -> Result<()> {
let context = TestContext::new("3.12");

fs_err::create_dir(context.temp_dir.child("script"))?;

uv_snapshot!(context.filters(), context.run().arg("--script").arg("script"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: failed to read from file `script`
Caused by: Is a directory (os error 21)
"###);

Ok(())
}
4 changes: 4 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ uv run [OPTIONS] <COMMAND>

<li><code>lowest-direct</code>: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies</li>
</ul>
</dd><dt><code>--script</code>, <code>-s</code></dt><dd><p>Run the given path as a Python script.</p>

<p>Using <code>--script</code> will attempt to parse the path as a PEP 723 script, irrespective of its extension.</p>

</dd><dt><code>--upgrade</code>, <code>-U</code></dt><dd><p>Allow package upgrades, ignoring pinned versions in any existing output file. Implies <code>--refresh</code></p>

</dd><dt><code>--upgrade-package</code>, <code>-P</code> <i>upgrade-package</i></dt><dd><p>Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies <code>--refresh-package</code></p>
Expand Down

0 comments on commit 5ff7dc9

Please sign in to comment.