Skip to content

Commit

Permalink
Add --group and --only-group to uv run
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Oct 17, 2024
1 parent 3ae3438 commit a9c1cca
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 16 deletions.
14 changes: 14 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2611,6 +2611,20 @@ pub struct RunArgs {
#[arg(long, overrides_with("dev"))]
pub no_dev: bool,

/// Include dependencies from the specified local dependency group.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("only_group"))]
pub group: Vec<GroupName>,

/// Only include dependencies from the specified local dependency group.
///
/// May be provided multiple times.
///
/// The project itself will also be omitted.
#[arg(long, conflicts_with("group"))]
pub only_group: Vec<GroupName>,

/// Run a Python module.
///
/// Equivalent to `python -m <module>`.
Expand Down
32 changes: 32 additions & 0 deletions crates/uv-configuration/src/dev.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::borrow::Cow;

use either::Either;
use uv_normalize::{GroupName, DEV_DEPENDENCIES};

Expand Down Expand Up @@ -25,6 +27,15 @@ impl DevMode {
pub fn prod(&self) -> bool {
matches!(self, Self::Exclude | Self::Include)
}

/// Returns the flag that was used to request development dependencies.
pub fn as_flag(&self) -> &'static str {
match self {
Self::Exclude => "--no-dev",
Self::Include => "--dev",
Self::Only => "--only-dev",
}
}
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -63,6 +74,23 @@ impl GroupsSpecification {
pub fn prod(&self) -> bool {
matches!(self, Self::Exclude | Self::Include(_))
}

/// Returns the option that was used to request the groups, if any.
pub fn as_flag(&self) -> Option<Cow<'_, str>> {
match self {
Self::Exclude => None,
Self::Include(groups) => match groups.as_slice() {
[] => None,
[group] => Some(Cow::Owned(format!("--group {group}"))),
[..] => Some(Cow::Borrowed("--group")),
},
Self::Only(groups) => match groups.as_slice() {
[] => None,
[group] => Some(Cow::Owned(format!("--only-group {group}"))),
[..] => Some(Cow::Borrowed("--only-group")),
},
}
}
}

impl DevGroupsSpecification {
Expand Down Expand Up @@ -134,6 +162,10 @@ impl DevGroupsSpecification {
pub fn dev_mode(&self) -> Option<&DevMode> {
self.dev.as_ref()
}

pub fn groups(&self) -> &GroupsSpecification {
&self.groups
}
}

impl From<DevMode> for DevGroupsSpecification {
Expand Down
37 changes: 23 additions & 14 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use uv_cache::Cache;
use uv_cli::ExternalCommand;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{
Concurrency, DevGroupsSpecification, DevMode, EditableMode, ExtrasSpecification,
InstallOptions, LowerBound, SourceStrategy,
Concurrency, DevGroupsSpecification, EditableMode, ExtrasSpecification, InstallOptions,
LowerBound, SourceStrategy,
};
use uv_distribution::LoweredRequirement;
use uv_fs::which::is_executable;
Expand Down Expand Up @@ -336,11 +336,14 @@ pub(crate) async fn run(
if !extras.is_empty() {
warn_user!("Extras are not supported for Python scripts with inline metadata");
}
if matches!(dev.dev_mode(), Some(DevMode::Exclude)) {
warn_user!("`--no-dev` is not supported for Python scripts with inline metadata");
if let Some(dev_mode) = dev.dev_mode() {
warn_user!(
"`{}` is not supported for Python scripts with inline metadata",
dev_mode.as_flag()
);
}
if matches!(dev.dev_mode(), Some(DevMode::Only)) {
warn_user!("`--only-dev` is not supported for Python scripts with inline metadata");
if let Some(flag) = dev.groups().as_flag() {
warn_user!("`{flag}` is not supported for Python scripts with inline metadata");
}
if package.is_some() {
warn_user!(
Expand Down Expand Up @@ -413,11 +416,14 @@ pub(crate) async fn run(
if !extras.is_empty() {
warn_user!("Extras have no effect when used alongside `--no-project`");
}
if matches!(dev.dev_mode(), Some(DevMode::Exclude)) {
warn_user!("`--no-dev` has no effect when used alongside `--no-project`");
if let Some(dev_mode) = dev.dev_mode() {
warn_user!(
"`{}` has no effect when used alongside `--no-project`",
dev_mode.as_flag()
);
}
if matches!(dev.dev_mode(), Some(DevMode::Only)) {
warn_user!("`--only-dev` has no effect when used alongside `--no-project`");
if let Some(flag) = dev.groups().as_flag() {
warn_user!("`{flag}` has no effect when used alongside `--no-project`");
}
if locked {
warn_user!("`--locked` has no effect when used alongside `--no-project`");
Expand All @@ -433,11 +439,14 @@ pub(crate) async fn run(
if !extras.is_empty() {
warn_user!("Extras have no effect when used outside of a project");
}
if matches!(dev.dev_mode(), Some(DevMode::Exclude)) {
warn_user!("`--no-dev` has no effect when used outside of a project");
if let Some(dev_mode) = dev.dev_mode() {
warn_user!(
"`{}` has no effect when used outside of a project",
dev_mode.as_flag()
);
}
if matches!(dev.dev_mode(), Some(DevMode::Only)) {
warn_user!("`--only-dev` has no effect when used outside of a project");
if let Some(flag) = dev.groups().as_flag() {
warn_user!("`{flag}` has no effect when used outside of a project");
}
if locked {
warn_user!("`--locked` has no effect when used outside of a project");
Expand Down
5 changes: 3 additions & 2 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ impl RunSettings {
no_all_extras,
dev,
no_dev,
group,
only_group,
module: _,
only_dev,
no_editable,
Expand Down Expand Up @@ -280,8 +282,7 @@ impl RunSettings {
flag(all_extras, no_all_extras).unwrap_or_default(),
extra.unwrap_or_default(),
),
// TODO(zanieb): Support `--group` here
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, vec![], vec![]),
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
editable: EditableMode::from_args(no_editable),
with,
with_editable,
Expand Down
179 changes: 179 additions & 0 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,30 @@ fn run_pep723_script() -> Result<()> {
"#
})?;

// Running a script with `--group` should warn.
uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Reading inline script metadata from `main.py`
× No solution found when resolving script dependencies:
╰─▶ Because there are no versions of add and you require add, we can conclude that your requirements are unsatisfiable.
"###);

// If the script can't be resolved, we should reference the script.
let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "add",
# ]
# ///
"#
})?;

uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("main.py"), @r###"
success: false
exit_code: 1
Expand Down Expand Up @@ -925,6 +949,161 @@ fn run_with_editable() -> Result<()> {
Ok(())
}

#[test]
fn run_group() -> 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 = ["typing-extensions"]
[dependency-groups]
foo = ["anyio"]
bar = ["iniconfig"]
dev = ["sniffio"]
"#,
)?;

let test_script = context.temp_dir.child("main.py");
test_script.write_str(indoc! { r#"
try:
import anyio
print("imported `anyio`")
except ImportError:
print("failed to import `anyio`")
try:
import iniconfig
print("imported `iniconfig`")
except ImportError:
print("failed to import `iniconfig`")
try:
import typing_extensions
print("imported `typing_extensions`")
except ImportError:
print("failed to import `typing_extensions`")
"#
})?;

context.lock().assert().success();

uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
failed to import `anyio`
failed to import `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);

uv_snapshot!(context.filters(), context.run().arg("--only-group").arg("bar").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
failed to import `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);

uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
"###);

uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
Resolved 6 packages in [TIME]
Audited 5 packages in [TIME]
"###);

uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--no-project").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
warning: `--group foo` has no effect when used alongside `--no-project`
"###);

uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("--no-project").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
warning: `--group` has no effect when used alongside `--no-project`
"###);

uv_snapshot!(context.filters(), context.run().arg("--group").arg("dev").arg("--no-project").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
warning: `--group dev` has no effect when used alongside `--no-project`
"###);

uv_snapshot!(context.filters(), context.run().arg("--dev").arg("--no-project").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
imported `anyio`
imported `iniconfig`
imported `typing_extensions`
----- stderr -----
warning: `--dev` has no effect when used alongside `--no-project`
"###);

Ok(())
}

#[test]
fn run_locked() -> Result<()> {
let context = TestContext::new("3.12");
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ uv run [OPTIONS] [COMMAND]

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

</dd><dt><code>--group</code> <i>group</i></dt><dd><p>Include dependencies from the specified local dependency group.</p>

<p>May be provided multiple times.</p>

</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>

</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>
Expand Down Expand Up @@ -307,6 +311,12 @@ uv run [OPTIONS] [COMMAND]

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

</dd><dt><code>--only-group</code> <i>only-group</i></dt><dd><p>Only include dependencies from the specified local dependency group.</p>

<p>May be provided multiple times.</p>

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

</dd><dt><code>--package</code> <i>package</i></dt><dd><p>Run the command in a specific package in the workspace.</p>

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

0 comments on commit a9c1cca

Please sign in to comment.