From f563c15b6ea342c10ff8d36cb4de33d4c61cf684 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 31 Oct 2024 21:23:55 -0400 Subject: [PATCH] Add support for uv run --all --- crates/uv-cli/src/lib.rs | 12 +- crates/uv/src/commands/project/run.rs | 16 ++- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 3 + crates/uv/tests/it/run.rs | 163 ++++++++++++++++++++++++++ docs/reference/cli.md | 8 +- 6 files changed, 199 insertions(+), 4 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 52f7bff7a2ec1..bf2a044f4158b 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2740,10 +2740,20 @@ pub struct RunArgs { #[command(flatten)] pub refresh: RefreshArgs, + /// Run the command with all workspace members installed. + /// + /// The workspace's environment (`.venv`) is updated to include all workspace + /// members. + /// + /// Any extras or groups specified via `--extra`, `--group`, or related options + /// will be applied to all workspace members. + #[arg(long, conflicts_with = "package")] + pub all: bool, + /// Run the command in a specific package in the workspace. /// /// If the workspace member does not exist, uv will exit with an error. - #[arg(long)] + #[arg(long, conflicts_with = "all")] pub package: Option, /// Avoid discovering the project or workspace. diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index d8812403bebcd..e040bb48a5c59 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -65,6 +65,7 @@ pub(crate) async fn run( frozen: bool, no_sync: bool, isolated: bool, + all: bool, package: Option, no_project: bool, no_config: bool, @@ -346,6 +347,11 @@ pub(crate) async fn run( if let Some(flag) = dev.groups().and_then(GroupsSpecification::as_flag) { warn_user!("`{flag}` is not supported for Python scripts with inline metadata"); } + if all { + warn_user!( + "`--all` is a no-op for Python scripts with inline metadata, which always run in isolation" + ); + } if package.is_some() { warn_user!( "`--package` is a no-op for Python scripts with inline metadata, which always run in isolation" @@ -550,8 +556,14 @@ pub(crate) async fn run( .flatten(); } } else { + let target = if all { + InstallTarget::from_workspace(&project) + } else { + InstallTarget::from_project(&project) + }; + // Determine the default groups to include. - validate_dependency_groups(InstallTarget::from_project(&project), &dev)?; + validate_dependency_groups(target, &dev)?; let defaults = default_dependency_groups(project.pyproject_toml())?; // Determine the lock mode. @@ -607,7 +619,7 @@ pub(crate) async fn run( let install_options = InstallOptions::default(); project::sync::do_sync( - InstallTarget::from_project(&project), + target, &venv, result.lock(), &extras, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index d499f9d5cd06b..d663443b7fd9f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1293,6 +1293,7 @@ async fn run_project( args.frozen, args.no_sync, args.isolated, + args.all, args.package, args.no_project, no_config, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 8d1d13c6c36d7..182ff5fbd8f18 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -235,6 +235,7 @@ pub(crate) struct RunSettings { pub(crate) with_requirements: Vec, pub(crate) isolated: bool, pub(crate) show_resolution: bool, + pub(crate) all: bool, pub(crate) package: Option, pub(crate) no_project: bool, pub(crate) no_sync: bool, @@ -271,6 +272,7 @@ impl RunSettings { installer, build, refresh, + all, package, no_project, python, @@ -296,6 +298,7 @@ impl RunSettings { .collect(), isolated, show_resolution, + all, package, no_project, no_sync, diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 826982f1b8101..e0bfd76a5c582 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -806,6 +806,169 @@ fn run_with() -> Result<()> { Ok(()) } +/// Sync all members in a workspace. +#[test] +fn run_in_workspace() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio>3"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv.workspace] + members = ["child1", "child2"] + + [tool.uv.sources] + child1 = { workspace = true } + child2 = { workspace = true } + "#, + )?; + context + .temp_dir + .child("src") + .child("project") + .child("__init__.py") + .touch()?; + + let child1 = context.temp_dir.child("child1"); + child1.child("pyproject.toml").write_str( + r#" + [project] + name = "child1" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>1"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + child1 + .child("src") + .child("child1") + .child("__init__.py") + .touch()?; + + let child2 = context.temp_dir.child("child2"); + child2.child("pyproject.toml").write_str( + r#" + [project] + name = "child2" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions>4"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + child2 + .child("src") + .child("child2") + .child("__init__.py") + .touch()?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import anyio + " + })?; + + uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + sniffio==1.3.1 + "###); + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import iniconfig + " + })?; + + uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Audited 4 packages in [TIME] + Traceback (most recent call last): + File "[TEMP_DIR]/main.py", line 1, in + import iniconfig + ModuleNotFoundError: No module named 'iniconfig' + "###); + + uv_snapshot!(context.filters(), context.run().arg("--package").arg("child1").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + child1==0.1.0 (from file://[TEMP_DIR]/child1) + + iniconfig==2.0.0 + "###); + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import typing_extensions + " + })?; + + uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Audited 4 packages in [TIME] + Traceback (most recent call last): + File "[TEMP_DIR]/main.py", line 1, in + import typing_extensions + ModuleNotFoundError: No module named 'typing_extensions' + "###); + + uv_snapshot!(context.filters(), context.run().arg("--all").arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + child2==0.1.0 (from file://[TEMP_DIR]/child2) + + typing-extensions==4.10.0 + "###); + + Ok(()) +} + #[test] fn run_with_editable() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ceb1e41023118..8173c7490db66 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -72,7 +72,13 @@ uv run [OPTIONS] [COMMAND]

Options

-
--all-extras

Include all optional dependencies.

+
--all

Run the command with all workspace members installed.

+ +

The workspace’s environment (.venv) is updated to include all workspace members.

+ +

Any extras or groups specified via --extra, --group, or related options will be applied to all workspace members.

+ +
--all-extras

Include all optional dependencies.

Optional dependencies are defined via project.optional-dependencies in a pyproject.toml.