diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index ea64e7339..64276796e 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2788,6 +2788,20 @@ pub struct SyncArgs { #[arg(long, conflicts_with("no_dev"))] pub only_dev: bool, + /// Include dependencies from the specified local dependency group. + /// + /// May be provided multiple times. + #[arg(long, conflicts_with("only_group"))] + pub group: Vec, + + /// 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, + /// Install any editable dependencies, including the project and any workspace members, as /// non-editable. #[arg(long)] diff --git a/crates/uv-configuration/src/dev.rs b/crates/uv-configuration/src/dev.rs index ba4be1721..b0a0cf902 100644 --- a/crates/uv-configuration/src/dev.rs +++ b/crates/uv-configuration/src/dev.rs @@ -61,3 +61,39 @@ impl From for DevSpecification { } } } + +impl DevSpecification { + /// Determine the [`DevSpecification`] policy from the command-line arguments. + pub fn from_args( + dev: bool, + no_dev: bool, + only_dev: bool, + group: Vec, + only_group: Vec, + ) -> Self { + let from_mode = DevSpecification::from(DevMode::from_args(dev, no_dev, only_dev)); + if !group.is_empty() { + match from_mode { + DevSpecification::Exclude => Self::Include(group), + DevSpecification::Include(dev) => { + Self::Include(group.into_iter().chain(dev).collect()) + } + DevSpecification::Only(_) => { + unreachable!("cannot specify both `--only-dev` and `--group`") + } + } + } else if !only_group.is_empty() { + match from_mode { + DevSpecification::Exclude => Self::Only(only_group), + DevSpecification::Only(dev) => { + Self::Only(only_group.into_iter().chain(dev).collect()) + } + // TODO(zanieb): `dev` defaults to true we can't tell if `--dev` was provided in + // conflict with `--only-group` here + DevSpecification::Include(_) => Self::Only(only_group), + } + } else { + from_mode + } + } +} diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index bc5bb4482..eba093b83 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -302,7 +302,13 @@ async fn do_lock( let requirements = workspace.non_project_requirements().collect::>(); let overrides = workspace.overrides().into_iter().collect::>(); let constraints = workspace.constraints(); - let dev = vec![DEV_DEPENDENCIES.clone()]; + let dev: Vec<_> = workspace + .pyproject_toml() + .dependency_groups + .iter() + .flat_map(|groups| groups.keys().cloned()) + .chain(std::iter::once(DEV_DEPENDENCIES.clone())) + .collect(); let source_trees = vec![]; // Collect the list of members. diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index e6136e8e5..1ccc84855 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -8,7 +8,7 @@ use uv_auth::store_credentials; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DevMode, DevSpecification, EditableMode, ExtrasSpecification, + Concurrency, Constraints, DevSpecification, EditableMode, ExtrasSpecification, HashCheckingMode, InstallOptions, LowerBound, }; use uv_dispatch::BuildDispatch; @@ -43,7 +43,7 @@ pub(crate) async fn sync( frozen: bool, package: Option, extras: ExtrasSpecification, - dev: DevMode, + dev: DevSpecification, editable: EditableMode, install_options: InstallOptions, modifications: Modifications, @@ -155,7 +155,7 @@ pub(crate) async fn sync( &venv, &lock, &extras, - &DevSpecification::from(dev), + &dev, editable, install_options, modifications, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 7123491b6..96496b009 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -19,10 +19,10 @@ use uv_cli::{ }; use uv_client::Connectivity; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, DevMode, EditableMode, ExportFormat, - ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, - NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, SourceStrategy, TargetTriple, - TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, + BuildOptions, Concurrency, ConfigSettings, DevMode, DevSpecification, EditableMode, + ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, + KeyringProviderType, NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, + SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, }; use uv_distribution_types::{DependencyMetadata, Index, IndexLocations}; use uv_install_wheel::linker::LinkMode; @@ -693,7 +693,7 @@ pub(crate) struct SyncSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DevMode, + pub(crate) dev: DevSpecification, pub(crate) editable: EditableMode, pub(crate) install_options: InstallOptions, pub(crate) modifications: Modifications, @@ -714,6 +714,8 @@ impl SyncSettings { dev, no_dev, only_dev, + group, + only_group, no_editable, inexact, exact, @@ -741,7 +743,7 @@ impl SyncSettings { flag(all_extras, no_all_extras).unwrap_or_default(), extra.unwrap_or_default(), ), - dev: DevMode::from_args(dev, no_dev, only_dev), + dev: DevSpecification::from_args(dev, no_dev, only_dev, group, only_group), editable: EditableMode::from_args(no_editable), install_options: InstallOptions::new( no_install_project, diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index aa3841764..40731e20e 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -4247,8 +4247,12 @@ fn add_group() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Audited in [TIME] + Resolved 4 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + sniffio==1.3.1 "###); let pyproject_toml = context.read("pyproject.toml"); @@ -4272,14 +4276,19 @@ fn add_group() -> Result<()> { ); }); - uv_snapshot!(context.filters(), context.add().arg("trio").arg("--group").arg("test"), @r###" + uv_snapshot!(context.filters(), context.add().arg("requests").arg("--group").arg("test"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Audited in [TIME] + Resolved 8 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + requests==2.31.0 + + urllib3==2.2.1 "###); let pyproject_toml = context.read("pyproject.toml"); @@ -4298,7 +4307,7 @@ fn add_group() -> Result<()> { [dependency-groups] test = [ "anyio==3.7.0", - "trio", + "requests>=2.31.0", ] "### ); @@ -4310,8 +4319,8 @@ fn add_group() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Audited in [TIME] + Resolved 8 packages in [TIME] + Audited 3 packages in [TIME] "###); let pyproject_toml = context.read("pyproject.toml"); @@ -4330,7 +4339,7 @@ fn add_group() -> Result<()> { [dependency-groups] test = [ "anyio==3.7.0", - "trio", + "requests>=2.31.0", ] second = [ "anyio==3.7.0", diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index dda1d6d6b..39bb7d1ac 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -12846,7 +12846,7 @@ fn lock_named_index_cli() -> Result<()> { ----- stderr ----- error: Failed to build: `project @ file://[TEMP_DIR]/` - Caused by: Failed to parse entry for: `jinja2` + Caused by: Failed to parse entry: `jinja2` Caused by: Package `jinja2` references an undeclared index: `pytorch` "###); diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 095c0b4fe..e61e391c1 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1012,6 +1012,88 @@ fn sync_dev() -> Result<()> { Ok(()) } +#[test] +fn sync_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 = ["requests"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Prepared 4 packages in [TIME] + Uninstalled 3 packages in [TIME] + Installed 4 packages in [TIME] + - anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + requests==2.31.0 + - sniffio==1.3.1 + - typing-extensions==4.10.0 + + urllib3==2.2.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "###); + + Ok(()) +} + /// Regression test for . /// /// Previously, we would read metadata statically from pyproject.toml and write that to `uv.lock`. In diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 0ff684fbd..34a83c5ac 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1416,6 +1416,10 @@ uv sync [OPTIONS]

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 pyproject.toml includes changes to dependencies that have not been included in the lockfile yet, they will not be present in the environment.

May also be set with the UV_FROZEN environment variable.

+
--group group

Include dependencies from the specified local dependency group.

+ +

May be provided multiple times.

+
--help, -h

Display the concise help for this command

--index index

The URLs to use when resolving dependencies, in addition to the default index.

@@ -1555,6 +1559,12 @@ uv sync [OPTIONS]

The project itself will also be omitted.

+
--only-group only-group

Only include dependencies from the specified local dependency group.

+ +

May be provided multiple times.

+ +

The project itself will also be omitted.

+
--package package

Sync for a specific package in the workspace.

The workspace’s environment (.venv) is updated to reflect the subset of dependencies declared by the specified workspace member package.