From 70a05855d8203ee540302a1d5f5d40a70f13cc02 Mon Sep 17 00:00:00 2001 From: j178 <10510431+j178@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:07:22 +0800 Subject: [PATCH 1/4] Support `uv run -m foo` to run a module --- crates/uv-cli/src/lib.rs | 8 +++++- crates/uv/src/commands/project/run.rs | 41 +++++++++++++++++++++++---- crates/uv/src/lib.rs | 7 +++-- crates/uv/src/settings.rs | 1 + crates/uv/tests/run.rs | 15 ++++++++++ docs/reference/cli.md | 2 +- 6 files changed, 65 insertions(+), 9 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 2699a519009f..e9601522c249 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2469,6 +2469,12 @@ pub struct RunArgs { #[arg(long, overrides_with("dev"))] pub no_dev: bool, + /// Run a Python module. + /// + /// Equivalent to `python -m `. + #[arg(short = 'm')] + pub module: Option, + /// Omit non-development dependencies. /// /// The project itself will also be omitted. @@ -2485,7 +2491,7 @@ pub struct RunArgs { /// If the path to a Python script (i.e., ending in `.py`), it will be /// executed with the Python interpreter. #[command(subcommand)] - pub command: ExternalCommand, + pub command: Option, /// Run with the given packages installed. /// diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 2dc660e34c9c..307062441f58 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use std::ffi::OsString; use std::fmt::Write; use std::io::Read; +use std::ops::Deref; use std::path::{Path, PathBuf}; use anstream::eprint; @@ -832,6 +833,9 @@ pub(crate) enum RunCommand { Python(Vec), /// Execute a `python` script. PythonScript(PathBuf, Vec), + /// Search `sys.path` for the named module and execute its contents as the `__main__` module. + /// Equivalent to `python -m module`. + PythonModule(String, Vec), /// Execute a `pythonw` script (Windows only). PythonGuiScript(PathBuf, Vec), /// Execute a Python package containing a `__main__.py` file. @@ -856,6 +860,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(), @@ -878,6 +883,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(); @@ -944,6 +956,13 @@ impl std::fmt::Display for RunCommand { } Ok(()) } + Self::PythonModule(module, args) => { + write!(f, "python -m {module}")?; + for arg in args { + write!(f, " {}", arg.to_string_lossy())?; + } + Ok(()) + } Self::PythonGuiScript(target, args) => { write!(f, "pythonw {}", target.display())?; for arg in args { @@ -970,12 +989,24 @@ impl std::fmt::Display for RunCommand { } } -impl TryFrom<&ExternalCommand> for RunCommand { - type Error = std::io::Error; - - fn try_from(command: &ExternalCommand) -> Result { - let (target, args) = command.split(); +impl RunCommand { + pub(crate) fn from_args( + command: &Option, + module: Option, + ) -> anyhow::Result { + if let Some(module) = module { + let args: Vec = command + .as_ref() + .map(|cmd| cmd.deref().clone()) + .unwrap_or_default(); + return Ok(Self::PythonModule(module, args)); + } + let (target, args) = if let Some(command) = command { + command.split() + } else { + (None, &[][..]) + }; let Some(target) = target else { return Ok(Self::Empty); }; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index d46fa74747a1..b5f016145412 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -131,8 +131,11 @@ async fn run(cli: Cli) -> Result { // 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.clone())?) } else { None } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index bbb253c1ff8b..b4bb325c3333 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -245,6 +245,7 @@ impl RunSettings { no_all_extras, dev, no_dev, + module: _, only_dev, no_editable, command: _, diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index ed6514913689..7b91f1be5c4c 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1830,6 +1830,21 @@ 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 ----- + "#); +} + /// When the `pyproject.toml` file is invalid. #[test] fn run_project_toml_error() -> Result<()> { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2a7f325dfed8..e64605619670 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -65,7 +65,7 @@ Arguments following the command (or script) are not interpreted as arguments to

Usage

``` -uv run [OPTIONS] +uv run [OPTIONS] [COMMAND] ```

Options

From f1ce779f8796f52a4bccdb3df71f63bb721038df Mon Sep 17 00:00:00 2001 From: j178 <10510431+j178@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:17:40 +0800 Subject: [PATCH 2/4] A failing test --- crates/uv-cli/src/lib.rs | 2 +- crates/uv/tests/run.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index e9601522c249..9b41ec48de2f 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2501,7 +2501,7 @@ pub struct RunArgs { #[arg(long)] pub with: Vec, - /// 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 diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index 7b91f1be5c4c..0842a4cf25f6 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -1843,6 +1843,28 @@ fn run_module() { ----- 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. From a66bcc8f780aec81e77acaba2ee59d0be4bed62f Mon Sep 17 00:00:00 2001 From: j178 <10510431+j178@users.noreply.github.com> Date: Sat, 28 Sep 2024 15:05:52 +0800 Subject: [PATCH 3/4] Use `module: bool` --- crates/uv-cli/src/lib.rs | 6 +++--- crates/uv/src/commands/project/run.rs | 31 +++++++++------------------ crates/uv/src/lib.rs | 2 +- docs/reference/cli.md | 4 ++-- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 9b41ec48de2f..8ae7ff5d8b4f 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2473,8 +2473,8 @@ pub struct RunArgs { /// /// Equivalent to `python -m `. #[arg(short = 'm')] - pub module: Option, - + pub module: bool, + /// Omit non-development dependencies. /// /// The project itself will also be omitted. @@ -2491,7 +2491,7 @@ pub struct RunArgs { /// If the path to a Python script (i.e., ending in `.py`), it will be /// executed with the Python interpreter. #[command(subcommand)] - pub command: Option, + pub command: ExternalCommand, /// Run with the given packages installed. /// diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 307062441f58..b854f6c40faa 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -3,7 +3,6 @@ use std::collections::BTreeMap; use std::ffi::OsString; use std::fmt::Write; use std::io::Read; -use std::ops::Deref; use std::path::{Path, PathBuf}; use anstream::eprint; @@ -835,7 +834,7 @@ pub(crate) enum RunCommand { PythonScript(PathBuf, Vec), /// Search `sys.path` for the named module and execute its contents as the `__main__` module. /// Equivalent to `python -m module`. - PythonModule(String, Vec), + PythonModule(OsString, Vec), /// Execute a `pythonw` script (Windows only). PythonGuiScript(PathBuf, Vec), /// Execute a Python package containing a `__main__.py` file. @@ -957,7 +956,8 @@ impl std::fmt::Display for RunCommand { Ok(()) } Self::PythonModule(module, args) => { - write!(f, "python -m {module}")?; + write!(f, "python -m")?; + write!(f, " {}", module.to_string_lossy())?; for arg in args { write!(f, " {}", arg.to_string_lossy())?; } @@ -990,28 +990,17 @@ impl std::fmt::Display for RunCommand { } impl RunCommand { - pub(crate) fn from_args( - command: &Option, - module: Option, - ) -> anyhow::Result { - if let Some(module) = module { - let args: Vec = command - .as_ref() - .map(|cmd| cmd.deref().clone()) - .unwrap_or_default(); - return Ok(Self::PythonModule(module, args)); - } - - let (target, args) = if let Some(command) = command { - command.split() - } else { - (None, &[][..]) - }; + pub(crate) fn from_args(command: &ExternalCommand, module: bool) -> anyhow::Result { + 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); diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index b5f016145412..7eb87fcae77c 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -135,7 +135,7 @@ async fn run(cli: Cli) -> Result { command, module, .. }) = &**command { - Some(RunCommand::from_args(command, module.clone())?) + Some(RunCommand::from_args(command, *module)?) } else { None } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index e64605619670..aa92e38bce37 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -65,7 +65,7 @@ Arguments following the command (or script) are not interpreted as arguments to

Usage

``` -uv run [OPTIONS] [COMMAND] +uv run [OPTIONS] ```

Options

@@ -380,7 +380,7 @@ uv run [OPTIONS] [COMMAND]

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.

-
--with-editable with-editable

Run with the given packages installed as editables

+
--with-editable with-editable

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 dependencies are allowed to conflict with those specified by the project.

From 30d01f32de3683ad290d7fe3311ae7bd1120a4b2 Mon Sep 17 00:00:00 2001 From: j178 <10510431+j178@users.noreply.github.com> Date: Sat, 28 Sep 2024 15:26:11 +0800 Subject: [PATCH 4/4] Allow long `--module` --- crates/uv-cli/src/lib.rs | 2 +- docs/reference/cli.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 8ae7ff5d8b4f..039ad985c815 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2472,7 +2472,7 @@ pub struct RunArgs { /// Run a Python module. /// /// Equivalent to `python -m `. - #[arg(short = 'm')] + #[arg(short, long)] pub module: bool, /// Omit non-development dependencies. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index aa92e38bce37..9e92c6d841ca 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -217,6 +217,10 @@ uv run [OPTIONS]

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

+
--module, -m

Run a Python module.

+ +

Equivalent to python -m <module>.

+
--native-tls

Whether to load TLS certificates from the platform’s native certificate store.

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).