Support `uv run -m foo` to run a module (#7754)

## 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
This commit is contained in:
Jo 2024-09-28 23:07:21 +08:00 committed by GitHub
parent 1cae78dd03
commit 0dbf9ae4a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 81 additions and 10 deletions

View File

@ -2469,6 +2469,12 @@ pub struct RunArgs {
#[arg(long, overrides_with("dev"))] #[arg(long, overrides_with("dev"))]
pub no_dev: bool, pub no_dev: bool,
/// Run a Python module.
///
/// Equivalent to `python -m <module>`.
#[arg(short, long)]
pub module: bool,
/// Omit non-development dependencies. /// Omit non-development dependencies.
/// ///
/// The project itself will also be omitted. /// The project itself will also be omitted.
@ -2495,7 +2501,7 @@ pub struct RunArgs {
#[arg(long)] #[arg(long)]
pub with: Vec<String>, 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 /// When used in a project, these dependencies will be layered on top of
/// the project environment in a separate, ephemeral environment. These /// the project environment in a separate, ephemeral environment. These

View File

@ -832,6 +832,9 @@ pub(crate) enum RunCommand {
Python(Vec<OsString>), Python(Vec<OsString>),
/// Execute a `python` script. /// Execute a `python` script.
PythonScript(PathBuf, Vec<OsString>), 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). /// Execute a `pythonw` script (Windows only).
PythonGuiScript(PathBuf, Vec<OsString>), PythonGuiScript(PathBuf, Vec<OsString>),
/// Execute a Python package containing a `__main__.py` file. /// Execute a Python package containing a `__main__.py` file.
@ -856,6 +859,7 @@ impl RunCommand {
| Self::PythonPackage(..) | Self::PythonPackage(..)
| Self::PythonZipapp(..) | Self::PythonZipapp(..)
| Self::Empty => Cow::Borrowed("python"), | Self::Empty => Cow::Borrowed("python"),
Self::PythonModule(..) => Cow::Borrowed("python -m"),
Self::PythonGuiScript(..) => Cow::Borrowed("pythonw"), Self::PythonGuiScript(..) => Cow::Borrowed("pythonw"),
Self::PythonStdin(_) => Cow::Borrowed("python -c"), Self::PythonStdin(_) => Cow::Borrowed("python -c"),
Self::External(executable, _) => executable.to_string_lossy(), Self::External(executable, _) => executable.to_string_lossy(),
@ -878,6 +882,13 @@ impl RunCommand {
process.args(args); process.args(args);
process 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) => { Self::PythonGuiScript(target, args) => {
let python_executable = interpreter.sys_executable(); let python_executable = interpreter.sys_executable();
@ -944,6 +955,14 @@ impl std::fmt::Display for RunCommand {
} }
Ok(()) 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) => { Self::PythonGuiScript(target, args) => {
write!(f, "pythonw {}", target.display())?; write!(f, "pythonw {}", target.display())?;
for arg in args { for arg in args {
@ -970,17 +989,18 @@ impl std::fmt::Display for RunCommand {
} }
} }
impl TryFrom<&ExternalCommand> for RunCommand { impl RunCommand {
type Error = std::io::Error; pub(crate) fn from_args(command: &ExternalCommand, module: bool) -> anyhow::Result<Self> {
fn try_from(command: &ExternalCommand) -> Result<Self, Self::Error> {
let (target, args) = command.split(); let (target, args) = command.split();
let Some(target) = target else { let Some(target) = target else {
return Ok(Self::Empty); 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 metadata = target_path.metadata();
let is_file = metadata.as_ref().map_or(false, std::fs::Metadata::is_file); 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); let is_dir = metadata.as_ref().map_or(false, std::fs::Metadata::is_dir);

View File

@ -131,8 +131,11 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
// Parse the external command, if necessary. // Parse the external command, if necessary.
let run_command = if let Commands::Project(command) = &*cli.command { let run_command = if let Commands::Project(command) = &*cli.command {
if let ProjectCommand::Run(uv_cli::RunArgs { command, .. }) = &**command { if let ProjectCommand::Run(uv_cli::RunArgs {
Some(RunCommand::try_from(command)?) command, module, ..
}) = &**command
{
Some(RunCommand::from_args(command, *module)?)
} else { } else {
None None
} }

View File

@ -245,6 +245,7 @@ impl RunSettings {
no_all_extras, no_all_extras,
dev, dev,
no_dev, no_dev,
module: _,
only_dev, only_dev,
no_editable, no_editable,
command: _, command: _,

View File

@ -1830,6 +1830,43 @@ fn run_zipapp() -> Result<()> {
Ok(()) 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. /// When the `pyproject.toml` file is invalid.
#[test] #[test]
fn run_project_toml_error() -> Result<()> { fn run_project_toml_error() -> Result<()> {

View File

@ -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> <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> </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> <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>
@ -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> <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> <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>