Skip to content

Commit

Permalink
Support uv run -m foo to run a module (#7754)
Browse files Browse the repository at this point in the history
## Summary

This is another attempt using `module: bool` instead of `module:
Option<String>` following #7322.
The original PR can't be reopened after a force-push to the branch, I've
created this new PR.

Resolves #6638
  • Loading branch information
j178 committed Sep 28, 2024
1 parent 1cae78d commit 0dbf9ae
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 10 deletions.
8 changes: 7 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2469,6 +2469,12 @@ pub struct RunArgs {
#[arg(long, overrides_with("dev"))]
pub no_dev: bool,

/// Run a Python module.
///
/// Equivalent to `python -m <module>`.
#[arg(short, long)]
pub module: bool,

/// Omit non-development dependencies.
///
/// The project itself will also be omitted.
Expand All @@ -2495,7 +2501,7 @@ pub struct RunArgs {
#[arg(long)]
pub with: Vec<String>,

/// Run with the given packages installed as editables
/// Run with the given packages installed as editables.
///
/// When used in a project, these dependencies will be layered on top of
/// the project environment in a separate, ephemeral environment. These
Expand Down
32 changes: 26 additions & 6 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,9 @@ pub(crate) enum RunCommand {
Python(Vec<OsString>),
/// Execute a `python` script.
PythonScript(PathBuf, Vec<OsString>),
/// Search `sys.path` for the named module and execute its contents as the `__main__` module.
/// Equivalent to `python -m module`.
PythonModule(OsString, Vec<OsString>),
/// Execute a `pythonw` script (Windows only).
PythonGuiScript(PathBuf, Vec<OsString>),
/// Execute a Python package containing a `__main__.py` file.
Expand All @@ -856,6 +859,7 @@ impl RunCommand {
| Self::PythonPackage(..)
| Self::PythonZipapp(..)
| Self::Empty => Cow::Borrowed("python"),
Self::PythonModule(..) => Cow::Borrowed("python -m"),
Self::PythonGuiScript(..) => Cow::Borrowed("pythonw"),
Self::PythonStdin(_) => Cow::Borrowed("python -c"),
Self::External(executable, _) => executable.to_string_lossy(),
Expand All @@ -878,6 +882,13 @@ impl RunCommand {
process.args(args);
process
}
Self::PythonModule(module, args) => {
let mut process = Command::new(interpreter.sys_executable());
process.arg("-m");
process.arg(module);
process.args(args);
process
}
Self::PythonGuiScript(target, args) => {
let python_executable = interpreter.sys_executable();

Expand Down Expand Up @@ -944,6 +955,14 @@ impl std::fmt::Display for RunCommand {
}
Ok(())
}
Self::PythonModule(module, args) => {
write!(f, "python -m")?;
write!(f, " {}", module.to_string_lossy())?;
for arg in args {
write!(f, " {}", arg.to_string_lossy())?;
}
Ok(())
}
Self::PythonGuiScript(target, args) => {
write!(f, "pythonw {}", target.display())?;
for arg in args {
Expand All @@ -970,17 +989,18 @@ impl std::fmt::Display for RunCommand {
}
}

impl TryFrom<&ExternalCommand> for RunCommand {
type Error = std::io::Error;

fn try_from(command: &ExternalCommand) -> Result<Self, Self::Error> {
impl RunCommand {
pub(crate) fn from_args(command: &ExternalCommand, module: bool) -> anyhow::Result<Self> {
let (target, args) = command.split();

let Some(target) = target else {
return Ok(Self::Empty);
};

let target_path = PathBuf::from(&target);
if module {
return Ok(Self::PythonModule(target.clone(), args.to_vec()));
}

let target_path = PathBuf::from(target);
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);
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 @@ -131,8 +131,11 @@ 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, .. }) = &**command {
Some(RunCommand::try_from(command)?)
if let ProjectCommand::Run(uv_cli::RunArgs {
command, module, ..
}) = &**command
{
Some(RunCommand::from_args(command, *module)?)
} 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 @@ -245,6 +245,7 @@ impl RunSettings {
no_all_extras,
dev,
no_dev,
module: _,
only_dev,
no_editable,
command: _,
Expand Down
37 changes: 37 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,43 @@ fn run_zipapp() -> Result<()> {
Ok(())
}

/// Run a module equivalent to `python -m foo`.
#[test]
fn run_module() {
let context = TestContext::new("3.12");

uv_snapshot!(context.filters(), context.run().arg("-m").arg("__hello__"), @r#"
success: true
exit_code: 0
----- stdout -----
Hello world!
----- stderr -----
"#);

uv_snapshot!(context.filters(), context.run().arg("-m").arg("http.server").arg("-h"), @r#"
success: true
exit_code: 0
----- stdout -----
usage: server.py [-h] [--cgi] [-b ADDRESS] [-d DIRECTORY] [-p VERSION] [port]
positional arguments:
port bind to this port (default: 8000)
options:
-h, --help show this help message and exit
--cgi run as CGI server
-b ADDRESS, --bind ADDRESS
bind to this address (default: all interfaces)
-d DIRECTORY, --directory DIRECTORY
serve this directory (default: current directory)
-p VERSION, --protocol VERSION
conform to this HTTP version (default: HTTP/1.0)
----- stderr -----
"#);
}

/// When the `pyproject.toml` file is invalid.
#[test]
fn run_project_toml_error() -> Result<()> {
Expand Down
6 changes: 5 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ uv run [OPTIONS] <COMMAND>

<p>Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, uv will exit with an error.</p>

</dd><dt><code>--module</code>, <code>-m</code></dt><dd><p>Run a Python module.</p>

<p>Equivalent to <code>python -m &lt;module&gt;</code>.</p>

</dd><dt><code>--native-tls</code></dt><dd><p>Whether to load TLS certificates from the platform&#8217;s native certificate store.</p>

<p>By default, uv loads certificates from the bundled <code>webpki-roots</code> crate. The <code>webpki-roots</code> are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).</p>
Expand Down Expand Up @@ -380,7 +384,7 @@ uv run [OPTIONS] <COMMAND>

<p>When used in a project, these dependencies will be layered on top of the project environment in a separate, ephemeral environment. These dependencies are allowed to conflict with those specified by the project.</p>

</dd><dt><code>--with-editable</code> <i>with-editable</i></dt><dd><p>Run with the given packages installed as editables</p>
</dd><dt><code>--with-editable</code> <i>with-editable</i></dt><dd><p>Run with the given packages installed as editables.</p>

<p>When used in a project, these dependencies will be layered on top of the project environment in a separate, ephemeral environment. These dependencies are allowed to conflict with those specified by the project.</p>

Expand Down

0 comments on commit 0dbf9ae

Please sign in to comment.