From 0dbf9ae4a746afb3a6ba85fc0fed460ff06a7f8d Mon Sep 17 00:00:00 2001 From: Jo <10510431+j178@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:07:21 +0800 Subject: [PATCH] Support `uv run -m foo` to run a module (#7754) ## Summary This is another attempt using `module: bool` instead of `module: Option` following #7322. The original PR can't be reopened after a force-push to the branch, I've created this new PR. Resolves #6638 --- crates/uv-cli/src/lib.rs | 8 +++++- crates/uv/src/commands/project/run.rs | 32 ++++++++++++++++++----- crates/uv/src/lib.rs | 7 +++-- crates/uv/src/settings.rs | 1 + crates/uv/tests/run.rs | 37 +++++++++++++++++++++++++++ docs/reference/cli.md | 6 ++++- 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 2699a519009f..039ad985c815 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, long)] + pub module: bool, + /// Omit non-development dependencies. /// /// The project itself will also be omitted. @@ -2495,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/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 2dc660e34c9c..b854f6c40faa 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -832,6 +832,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(OsString, Vec), /// Execute a `pythonw` script (Windows only). PythonGuiScript(PathBuf, Vec), /// Execute a Python package containing a `__main__.py` file. @@ -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(), @@ -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(); @@ -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 { @@ -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 { +impl RunCommand { + 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 d46fa74747a1..7eb87fcae77c 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)?) } 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..0842a4cf25f6 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -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<()> { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2a7f325dfed8..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).

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

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.