Skip to content

Commit 0a6d802

Browse files
committed
Add support for uv run --all
1 parent 1e997d5 commit 0a6d802

File tree

6 files changed

+199
-4
lines changed

6 files changed

+199
-4
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2740,10 +2740,20 @@ pub struct RunArgs {
27402740
#[command(flatten)]
27412741
pub refresh: RefreshArgs,
27422742

2743+
/// Run the command with all workspace members installed.
2744+
///
2745+
/// The workspace's environment (`.venv`) is updated to include all workspace
2746+
/// members.
2747+
///
2748+
/// Any extras or groups specified via `--extra`, `--group`, or related options
2749+
/// will be applied to all workspace members.
2750+
#[arg(long, conflicts_with = "package")]
2751+
pub all: bool,
2752+
27432753
/// Run the command in a specific package in the workspace.
27442754
///
27452755
/// If the workspace member does not exist, uv will exit with an error.
2746-
#[arg(long)]
2756+
#[arg(long, conflicts_with = "all")]
27472757
pub package: Option<PackageName>,
27482758

27492759
/// Avoid discovering the project or workspace.

crates/uv/src/commands/project/run.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ pub(crate) async fn run(
6565
frozen: bool,
6666
no_sync: bool,
6767
isolated: bool,
68+
all: bool,
6869
package: Option<PackageName>,
6970
no_project: bool,
7071
no_config: bool,
@@ -346,6 +347,11 @@ pub(crate) async fn run(
346347
if let Some(flag) = dev.groups().and_then(GroupsSpecification::as_flag) {
347348
warn_user!("`{flag}` is not supported for Python scripts with inline metadata");
348349
}
350+
if all {
351+
warn_user!(
352+
"`--all` is a no-op for Python scripts with inline metadata, which always run in isolation"
353+
);
354+
}
349355
if package.is_some() {
350356
warn_user!(
351357
"`--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(
550556
.flatten();
551557
}
552558
} else {
559+
let target = if all {
560+
InstallTarget::from_workspace(&project)
561+
} else {
562+
InstallTarget::from_project(&project)
563+
};
564+
553565
// Determine the default groups to include.
554-
validate_dependency_groups(InstallTarget::from_project(&project), &dev)?;
566+
validate_dependency_groups(target, &dev)?;
555567
let defaults = default_dependency_groups(project.pyproject_toml())?;
556568

557569
// Determine the lock mode.
@@ -607,7 +619,7 @@ pub(crate) async fn run(
607619
let install_options = InstallOptions::default();
608620

609621
project::sync::do_sync(
610-
InstallTarget::from_project(&project),
622+
target,
611623
&venv,
612624
result.lock(),
613625
&extras,

crates/uv/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,7 @@ async fn run_project(
12931293
args.frozen,
12941294
args.no_sync,
12951295
args.isolated,
1296+
args.all,
12961297
args.package,
12971298
args.no_project,
12981299
no_config,

crates/uv/src/settings.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ pub(crate) struct RunSettings {
235235
pub(crate) with_requirements: Vec<PathBuf>,
236236
pub(crate) isolated: bool,
237237
pub(crate) show_resolution: bool,
238+
pub(crate) all: bool,
238239
pub(crate) package: Option<PackageName>,
239240
pub(crate) no_project: bool,
240241
pub(crate) no_sync: bool,
@@ -271,6 +272,7 @@ impl RunSettings {
271272
installer,
272273
build,
273274
refresh,
275+
all,
274276
package,
275277
no_project,
276278
python,
@@ -296,6 +298,7 @@ impl RunSettings {
296298
.collect(),
297299
isolated,
298300
show_resolution,
301+
all,
299302
package,
300303
no_project,
301304
no_sync,

crates/uv/tests/it/run.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,169 @@ fn run_with() -> Result<()> {
806806
Ok(())
807807
}
808808

809+
/// Sync all members in a workspace.
810+
#[test]
811+
fn run_in_workspace() -> Result<()> {
812+
let context = TestContext::new("3.12");
813+
814+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
815+
pyproject_toml.write_str(
816+
r#"
817+
[project]
818+
name = "project"
819+
version = "0.1.0"
820+
requires-python = ">=3.12"
821+
dependencies = ["anyio>3"]
822+
823+
[build-system]
824+
requires = ["setuptools>=42"]
825+
build-backend = "setuptools.build_meta"
826+
827+
[tool.uv.workspace]
828+
members = ["child1", "child2"]
829+
830+
[tool.uv.sources]
831+
child1 = { workspace = true }
832+
child2 = { workspace = true }
833+
"#,
834+
)?;
835+
context
836+
.temp_dir
837+
.child("src")
838+
.child("project")
839+
.child("__init__.py")
840+
.touch()?;
841+
842+
let child1 = context.temp_dir.child("child1");
843+
child1.child("pyproject.toml").write_str(
844+
r#"
845+
[project]
846+
name = "child1"
847+
version = "0.1.0"
848+
requires-python = ">=3.12"
849+
dependencies = ["iniconfig>1"]
850+
851+
[build-system]
852+
requires = ["setuptools>=42"]
853+
build-backend = "setuptools.build_meta"
854+
"#,
855+
)?;
856+
child1
857+
.child("src")
858+
.child("child1")
859+
.child("__init__.py")
860+
.touch()?;
861+
862+
let child2 = context.temp_dir.child("child2");
863+
child2.child("pyproject.toml").write_str(
864+
r#"
865+
[project]
866+
name = "child2"
867+
version = "0.1.0"
868+
requires-python = ">=3.12"
869+
dependencies = ["typing-extensions>4"]
870+
871+
[build-system]
872+
requires = ["setuptools>=42"]
873+
build-backend = "setuptools.build_meta"
874+
"#,
875+
)?;
876+
child2
877+
.child("src")
878+
.child("child2")
879+
.child("__init__.py")
880+
.touch()?;
881+
882+
let test_script = context.temp_dir.child("main.py");
883+
test_script.write_str(indoc! { r"
884+
import anyio
885+
"
886+
})?;
887+
888+
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
889+
success: true
890+
exit_code: 0
891+
----- stdout -----
892+
893+
----- stderr -----
894+
Resolved 8 packages in [TIME]
895+
Prepared 4 packages in [TIME]
896+
Installed 4 packages in [TIME]
897+
+ anyio==4.3.0
898+
+ idna==3.6
899+
+ project==0.1.0 (from file://[TEMP_DIR]/)
900+
+ sniffio==1.3.1
901+
"###);
902+
903+
let test_script = context.temp_dir.child("main.py");
904+
test_script.write_str(indoc! { r"
905+
import iniconfig
906+
"
907+
})?;
908+
909+
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
910+
success: false
911+
exit_code: 1
912+
----- stdout -----
913+
914+
----- stderr -----
915+
Resolved 8 packages in [TIME]
916+
Audited 4 packages in [TIME]
917+
Traceback (most recent call last):
918+
File "[TEMP_DIR]/main.py", line 1, in <module>
919+
import iniconfig
920+
ModuleNotFoundError: No module named 'iniconfig'
921+
"###);
922+
923+
uv_snapshot!(context.filters(), context.run().arg("--package").arg("child1").arg("main.py"), @r###"
924+
success: true
925+
exit_code: 0
926+
----- stdout -----
927+
928+
----- stderr -----
929+
Resolved 8 packages in [TIME]
930+
Prepared 2 packages in [TIME]
931+
Installed 2 packages in [TIME]
932+
+ child1==0.1.0 (from file://[TEMP_DIR]/child1)
933+
+ iniconfig==2.0.0
934+
"###);
935+
936+
let test_script = context.temp_dir.child("main.py");
937+
test_script.write_str(indoc! { r"
938+
import typing_extensions
939+
"
940+
})?;
941+
942+
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
943+
success: false
944+
exit_code: 1
945+
----- stdout -----
946+
947+
----- stderr -----
948+
Resolved 8 packages in [TIME]
949+
Audited 4 packages in [TIME]
950+
Traceback (most recent call last):
951+
File "[TEMP_DIR]/main.py", line 1, in <module>
952+
import typing_extensions
953+
ModuleNotFoundError: No module named 'typing_extensions'
954+
"###);
955+
956+
uv_snapshot!(context.filters(), context.run().arg("--all").arg("main.py"), @r###"
957+
success: true
958+
exit_code: 0
959+
----- stdout -----
960+
961+
----- stderr -----
962+
Resolved 8 packages in [TIME]
963+
Prepared 2 packages in [TIME]
964+
Installed 2 packages in [TIME]
965+
+ child2==0.1.0 (from file://[TEMP_DIR]/child2)
966+
+ typing-extensions==4.10.0
967+
"###);
968+
969+
Ok(())
970+
}
971+
809972
#[test]
810973
fn run_with_editable() -> Result<()> {
811974
let context = TestContext::new("3.12");

docs/reference/cli.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,13 @@ uv run [OPTIONS] [COMMAND]
7272

7373
<h3 class="cli-reference">Options</h3>
7474

75-
<dl class="cli-reference"><dt><code>--all-extras</code></dt><dd><p>Include all optional dependencies.</p>
75+
<dl class="cli-reference"><dt><code>--all</code></dt><dd><p>Run the command with all workspace members installed.</p>
76+
77+
<p>The workspace&#8217;s environment (<code>.venv</code>) is updated to include all workspace members.</p>
78+
79+
<p>Any extras or groups specified via <code>--extra</code>, <code>--group</code>, or related options will be applied to all workspace members.</p>
80+
81+
</dd><dt><code>--all-extras</code></dt><dd><p>Include all optional dependencies.</p>
7682

7783
<p>Optional dependencies are defined via <code>project.optional-dependencies</code> in a <code>pyproject.toml</code>.</p>
7884

0 commit comments

Comments
 (0)