Skip to content

Commit 7259faf

Browse files
authored
Add --group and --only-group to uv run (#8274)
Similar to #8110 Part of #8090
1 parent e3f4abe commit 7259faf

File tree

6 files changed

+261
-16
lines changed

6 files changed

+261
-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 {
@@ -138,6 +166,10 @@ impl DevGroupsSpecification {
138166
pub fn dev_mode(&self) -> Option<&DevMode> {
139167
self.dev.as_ref()
140168
}
169+
170+
pub fn groups(&self) -> &GroupsSpecification {
171+
&self.groups
172+
}
141173
}
142174

143175
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: 179 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,161 @@ 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 = ["iniconfig"]
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 iniconfig
982+
print("imported `iniconfig`")
983+
except ImportError:
984+
print("failed to import `iniconfig`")
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 `iniconfig`
1002+
imported `typing_extensions`
1003+
1004+
----- stderr -----
1005+
Resolved 6 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 `iniconfig`
1018+
imported `typing_extensions`
1019+
1020+
----- stderr -----
1021+
Resolved 6 packages in [TIME]
1022+
Prepared 1 package in [TIME]
1023+
Installed 1 package in [TIME]
1024+
+ iniconfig==2.0.0
1025+
"###);
1026+
1027+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("main.py"), @r###"
1028+
success: true
1029+
exit_code: 0
1030+
----- stdout -----
1031+
imported `anyio`
1032+
imported `iniconfig`
1033+
imported `typing_extensions`
1034+
1035+
----- stderr -----
1036+
Resolved 6 packages in [TIME]
1037+
Prepared 2 packages in [TIME]
1038+
Installed 2 packages in [TIME]
1039+
+ anyio==4.3.0
1040+
+ idna==3.6
1041+
"###);
1042+
1043+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("main.py"), @r###"
1044+
success: true
1045+
exit_code: 0
1046+
----- stdout -----
1047+
imported `anyio`
1048+
imported `iniconfig`
1049+
imported `typing_extensions`
1050+
1051+
----- stderr -----
1052+
Resolved 6 packages in [TIME]
1053+
Audited 5 packages in [TIME]
1054+
"###);
1055+
1056+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--no-project").arg("main.py"), @r###"
1057+
success: true
1058+
exit_code: 0
1059+
----- stdout -----
1060+
imported `anyio`
1061+
imported `iniconfig`
1062+
imported `typing_extensions`
1063+
1064+
----- stderr -----
1065+
warning: `--group foo` has no effect when used alongside `--no-project`
1066+
"###);
1067+
1068+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("--no-project").arg("main.py"), @r###"
1069+
success: true
1070+
exit_code: 0
1071+
----- stdout -----
1072+
imported `anyio`
1073+
imported `iniconfig`
1074+
imported `typing_extensions`
1075+
1076+
----- stderr -----
1077+
warning: `--group` has no effect when used alongside `--no-project`
1078+
"###);
1079+
1080+
uv_snapshot!(context.filters(), context.run().arg("--group").arg("dev").arg("--no-project").arg("main.py"), @r###"
1081+
success: true
1082+
exit_code: 0
1083+
----- stdout -----
1084+
imported `anyio`
1085+
imported `iniconfig`
1086+
imported `typing_extensions`
1087+
1088+
----- stderr -----
1089+
warning: `--group dev` has no effect when used alongside `--no-project`
1090+
"###);
1091+
1092+
uv_snapshot!(context.filters(), context.run().arg("--dev").arg("--no-project").arg("main.py"), @r###"
1093+
success: true
1094+
exit_code: 0
1095+
----- stdout -----
1096+
imported `anyio`
1097+
imported `iniconfig`
1098+
imported `typing_extensions`
1099+
1100+
----- stderr -----
1101+
warning: `--dev` has no effect when used alongside `--no-project`
1102+
"###);
1103+
1104+
Ok(())
1105+
}
1106+
9281107
#[test]
9291108
fn run_locked() -> Result<()> {
9301109
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)