Skip to content

Commit 1007c17

Browse files
committed
Add --group and --only-group to uv run
1 parent 3ae3438 commit 1007c17

File tree

6 files changed

+264
-16
lines changed

6 files changed

+264
-16
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2611,6 +2611,20 @@ pub struct RunArgs {
26112611
#[arg(long, overrides_with("dev"))]
26122612
pub no_dev: bool,
26132613

2614+
/// Include dependencies from the specified local dependency group.
2615+
///
2616+
/// May be provided multiple times.
2617+
#[arg(long, conflicts_with("only_group"))]
2618+
pub group: Vec<GroupName>,
2619+
2620+
/// Only include dependencies from the specified local dependency group.
2621+
///
2622+
/// May be provided multiple times.
2623+
///
2624+
/// The project itself will also be omitted.
2625+
#[arg(long, conflicts_with("group"))]
2626+
pub only_group: Vec<GroupName>,
2627+
26142628
/// Run a Python module.
26152629
///
26162630
/// Equivalent to `python -m <module>`.

crates/uv-configuration/src/dev.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::borrow::Cow;
2+
13
use either::Either;
24
use uv_normalize::{GroupName, DEV_DEPENDENCIES};
35

@@ -25,6 +27,15 @@ impl DevMode {
2527
pub fn prod(&self) -> bool {
2628
matches!(self, Self::Exclude | Self::Include)
2729
}
30+
31+
/// Returns the flag that was used to request development dependencies.
32+
pub fn as_flag(&self) -> &'static str {
33+
match self {
34+
Self::Exclude => "--no-dev",
35+
Self::Include => "--dev",
36+
Self::Only => "--only-dev",
37+
}
38+
}
2839
}
2940

3041
#[derive(Debug, Clone)]
@@ -63,6 +74,23 @@ impl GroupsSpecification {
6374
pub fn prod(&self) -> bool {
6475
matches!(self, Self::Exclude | Self::Include(_))
6576
}
77+
78+
/// Returns the option that was used to request the groups, if any.
79+
pub fn as_flag(&self) -> Option<Cow<'_, str>> {
80+
match self {
81+
Self::Exclude => None,
82+
Self::Include(groups) => match groups.as_slice() {
83+
[] => None,
84+
[group] => Some(Cow::Owned(format!("--group {group}"))),
85+
[..] => Some(Cow::Borrowed("--group")),
86+
},
87+
Self::Only(groups) => match groups.as_slice() {
88+
[] => None,
89+
[group] => Some(Cow::Owned(format!("--only-group {group}"))),
90+
[..] => Some(Cow::Borrowed("--only-group")),
91+
},
92+
}
93+
}
6694
}
6795

6896
impl DevGroupsSpecification {
@@ -134,6 +162,10 @@ impl DevGroupsSpecification {
134162
pub fn dev_mode(&self) -> Option<&DevMode> {
135163
self.dev.as_ref()
136164
}
165+
166+
pub fn groups(&self) -> &GroupsSpecification {
167+
&self.groups
168+
}
137169
}
138170

139171
impl From<DevMode> for DevGroupsSpecification {

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

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use uv_cache::Cache;
1717
use uv_cli::ExternalCommand;
1818
use uv_client::{BaseClientBuilder, Connectivity};
1919
use uv_configuration::{
20-
Concurrency, DevGroupsSpecification, DevMode, EditableMode, ExtrasSpecification,
21-
InstallOptions, LowerBound, SourceStrategy,
20+
Concurrency, DevGroupsSpecification, EditableMode, ExtrasSpecification, InstallOptions,
21+
LowerBound, SourceStrategy,
2222
};
2323
use uv_distribution::LoweredRequirement;
2424
use uv_fs::which::is_executable;
@@ -336,11 +336,14 @@ pub(crate) async fn run(
336336
if !extras.is_empty() {
337337
warn_user!("Extras are not supported for Python scripts with inline metadata");
338338
}
339-
if matches!(dev.dev_mode(), Some(DevMode::Exclude)) {
340-
warn_user!("`--no-dev` is not supported for Python scripts with inline metadata");
339+
if let Some(dev_mode) = dev.dev_mode() {
340+
warn_user!(
341+
"`{}` is not supported for Python scripts with inline metadata",
342+
dev_mode.as_flag()
343+
);
341344
}
342-
if matches!(dev.dev_mode(), Some(DevMode::Only)) {
343-
warn_user!("`--only-dev` is not supported for Python scripts with inline metadata");
345+
if let Some(flag) = dev.groups().as_flag() {
346+
warn_user!("`{flag}` is not supported for Python scripts with inline metadata");
344347
}
345348
if package.is_some() {
346349
warn_user!(
@@ -413,11 +416,14 @@ pub(crate) async fn run(
413416
if !extras.is_empty() {
414417
warn_user!("Extras have no effect when used alongside `--no-project`");
415418
}
416-
if matches!(dev.dev_mode(), Some(DevMode::Exclude)) {
417-
warn_user!("`--no-dev` has no effect when used alongside `--no-project`");
419+
if let Some(dev_mode) = dev.dev_mode() {
420+
warn_user!(
421+
"`{}` has no effect when used alongside `--no-project`",
422+
dev_mode.as_flag()
423+
);
418424
}
419-
if matches!(dev.dev_mode(), Some(DevMode::Only)) {
420-
warn_user!("`--only-dev` has no effect when used alongside `--no-project`");
425+
if let Some(flag) = dev.groups().as_flag() {
426+
warn_user!("`{flag}` has no effect when used alongside `--no-project`");
421427
}
422428
if locked {
423429
warn_user!("`--locked` has no effect when used alongside `--no-project`");
@@ -433,11 +439,14 @@ pub(crate) async fn run(
433439
if !extras.is_empty() {
434440
warn_user!("Extras have no effect when used outside of a project");
435441
}
436-
if matches!(dev.dev_mode(), Some(DevMode::Exclude)) {
437-
warn_user!("`--no-dev` has no effect when used outside of a project");
442+
if let Some(dev_mode) = dev.dev_mode() {
443+
warn_user!(
444+
"`{}` has no effect when used outside of a project",
445+
dev_mode.as_flag()
446+
);
438447
}
439-
if matches!(dev.dev_mode(), Some(DevMode::Only)) {
440-
warn_user!("`--only-dev` has no effect when used outside of a project");
448+
if let Some(flag) = dev.groups().as_flag() {
449+
warn_user!("`{flag}` has no effect when used outside of a project");
441450
}
442451
if locked {
443452
warn_user!("`--locked` has no effect when used outside of a project");

crates/uv/src/settings.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ impl RunSettings {
252252
no_all_extras,
253253
dev,
254254
no_dev,
255+
group,
256+
only_group,
255257
module: _,
256258
only_dev,
257259
no_editable,
@@ -280,8 +282,7 @@ impl RunSettings {
280282
flag(all_extras, no_all_extras).unwrap_or_default(),
281283
extra.unwrap_or_default(),
282284
),
283-
// TODO(zanieb): Support `--group` here
284-
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, vec![], vec![]),
285+
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
285286
editable: EditableMode::from_args(no_editable),
286287
with,
287288
with_editable,

crates/uv/tests/it/run.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,30 @@ fn run_pep723_script() -> Result<()> {
416416
"#
417417
})?;
418418

419+
// Running a script with `--group` should warn.
420+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("main.py"), @r###"
421+
success: false
422+
exit_code: 1
423+
----- stdout -----
424+
425+
----- stderr -----
426+
Reading inline script metadata from `main.py`
427+
× No solution found when resolving script dependencies:
428+
╰─▶ Because there are no versions of add and you require add, we can conclude that your requirements are unsatisfiable.
429+
"###);
430+
431+
// If the script can't be resolved, we should reference the script.
432+
let test_script = context.temp_dir.child("main.py");
433+
test_script.write_str(indoc! { r#"
434+
# /// script
435+
# requires-python = ">=3.11"
436+
# dependencies = [
437+
# "add",
438+
# ]
439+
# ///
440+
"#
441+
})?;
442+
419443
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("main.py"), @r###"
420444
success: false
421445
exit_code: 1
@@ -925,6 +949,164 @@ fn run_with_editable() -> Result<()> {
925949
Ok(())
926950
}
927951

952+
#[test]
953+
fn run_group() -> Result<()> {
954+
let context = TestContext::new("3.12");
955+
956+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
957+
pyproject_toml.write_str(
958+
r#"
959+
[project]
960+
name = "project"
961+
version = "0.1.0"
962+
requires-python = ">=3.12"
963+
dependencies = ["typing-extensions"]
964+
965+
[dependency-groups]
966+
foo = ["anyio"]
967+
bar = ["trio"]
968+
dev = ["sniffio"]
969+
"#,
970+
)?;
971+
972+
let test_script = context.temp_dir.child("main.py");
973+
test_script.write_str(indoc! { r#"
974+
try:
975+
import anyio
976+
print("imported `anyio`")
977+
except ImportError:
978+
print("failed to import `anyio`")
979+
980+
try:
981+
import trio
982+
print("imported `trio`")
983+
except ImportError:
984+
print("failed to import `trio`")
985+
986+
try:
987+
import typing_extensions
988+
print("imported `typing_extensions`")
989+
except ImportError:
990+
print("failed to import `typing_extensions`")
991+
"#
992+
})?;
993+
994+
context.lock().assert().success();
995+
996+
uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
997+
success: true
998+
exit_code: 0
999+
----- stdout -----
1000+
failed to import `anyio`
1001+
failed to import `trio`
1002+
imported `typing_extensions`
1003+
1004+
----- stderr -----
1005+
Resolved 11 packages in [TIME]
1006+
Prepared 2 packages in [TIME]
1007+
Installed 2 packages in [TIME]
1008+
+ sniffio==1.3.1
1009+
+ typing-extensions==4.10.0
1010+
"###);
1011+
1012+
uv_snapshot!(context.filters(), context.run().arg("--only-group").arg("bar").arg("main.py"), @r###"
1013+
success: true
1014+
exit_code: 0
1015+
----- stdout -----
1016+
failed to import `anyio`
1017+
imported `trio`
1018+
imported `typing_extensions`
1019+
1020+
----- stderr -----
1021+
Resolved 11 packages in [TIME]
1022+
Prepared 5 packages in [TIME]
1023+
Installed 5 packages in [TIME]
1024+
+ attrs==23.2.0
1025+
+ idna==3.6
1026+
+ outcome==1.3.0.post0
1027+
+ sortedcontainers==2.4.0
1028+
+ trio==0.25.0
1029+
"###);
1030+
1031+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("main.py"), @r###"
1032+
success: true
1033+
exit_code: 0
1034+
----- stdout -----
1035+
imported `anyio`
1036+
imported `trio`
1037+
imported `typing_extensions`
1038+
1039+
----- stderr -----
1040+
Resolved 11 packages in [TIME]
1041+
Prepared 1 package in [TIME]
1042+
Installed 1 package in [TIME]
1043+
+ anyio==4.3.0
1044+
"###);
1045+
1046+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("main.py"), @r###"
1047+
success: true
1048+
exit_code: 0
1049+
----- stdout -----
1050+
imported `anyio`
1051+
imported `trio`
1052+
imported `typing_extensions`
1053+
1054+
----- stderr -----
1055+
Resolved 11 packages in [TIME]
1056+
Audited 8 packages in [TIME]
1057+
"###);
1058+
1059+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--no-project").arg("main.py"), @r###"
1060+
success: true
1061+
exit_code: 0
1062+
----- stdout -----
1063+
imported `anyio`
1064+
imported `trio`
1065+
imported `typing_extensions`
1066+
1067+
----- stderr -----
1068+
warning: `--group foo` has no effect when used alongside `--no-project`
1069+
"###);
1070+
1071+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("--no-project").arg("main.py"), @r###"
1072+
success: true
1073+
exit_code: 0
1074+
----- stdout -----
1075+
imported `anyio`
1076+
imported `trio`
1077+
imported `typing_extensions`
1078+
1079+
----- stderr -----
1080+
warning: `--group` has no effect when used alongside `--no-project`
1081+
"###);
1082+
1083+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("dev").arg("--no-project").arg("main.py"), @r###"
1084+
success: true
1085+
exit_code: 0
1086+
----- stdout -----
1087+
imported `anyio`
1088+
imported `trio`
1089+
imported `typing_extensions`
1090+
1091+
----- stderr -----
1092+
warning: `--group dev` has no effect when used alongside `--no-project`
1093+
"###);
1094+
1095+
uv_snapshot!(context.filters(), context.run().arg("--dev").arg("--no-project").arg("main.py"), @r###"
1096+
success: true
1097+
exit_code: 0
1098+
----- stdout -----
1099+
imported `anyio`
1100+
imported `trio`
1101+
imported `typing_extensions`
1102+
1103+
----- stderr -----
1104+
warning: `--dev` has no effect when used alongside `--no-project`
1105+
"###);
1106+
1107+
Ok(())
1108+
}
1109+
9281110
#[test]
9291111
fn run_locked() -> Result<()> {
9301112
let context = TestContext::new("3.12");

docs/reference/cli.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ uv run [OPTIONS] [COMMAND]
160160

161161
<p>Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the source of truth. If the lockfile is missing, uv will exit with an error. If the <code>pyproject.toml</code> includes changes to dependencies that have not been included in the lockfile yet, they will not be present in the environment.</p>
162162

163+
</dd><dt><code>--group</code> <i>group</i></dt><dd><p>Include dependencies from the specified local dependency group.</p>
164+
165+
<p>May be provided multiple times.</p>
166+
163167
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
164168

165169
</dd><dt><code>--index</code> <i>index</i></dt><dd><p>The URLs to use when resolving dependencies, in addition to the default index.</p>
@@ -307,6 +311,12 @@ uv run [OPTIONS] [COMMAND]
307311

308312
<p>The project itself will also be omitted.</p>
309313

314+
</dd><dt><code>--only-group</code> <i>only-group</i></dt><dd><p>Only include dependencies from the specified local dependency group.</p>
315+
316+
<p>May be provided multiple times.</p>
317+
318+
<p>The project itself will also be omitted.</p>
319+
310320
</dd><dt><code>--package</code> <i>package</i></dt><dd><p>Run the command in a specific package in the workspace.</p>
311321

312322
<p>If the workspace member does not exist, uv will exit with an error.</p>

0 commit comments

Comments
 (0)