diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index f1a0b2b72bec8..890382dc892de 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 ba4be17210c59..b0a0cf9029b2a 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 c80bd6726fec4..d9eef81b0408c 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -272,7 +272,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 8379721ffc476..6f60a888b0092 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, @@ -154,7 +154,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 0f917416968d5..03213c8cf57f4 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; @@ -690,7 +690,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, @@ -711,6 +711,8 @@ impl SyncSettings { dev, no_dev, only_dev, + group, + only_group, no_editable, inexact, exact, @@ -738,7 +740,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 62853b486611c..f7e91bb3225e9 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -4081,8 +4081,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"); @@ -4112,8 +4116,13 @@ fn add_group() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Audited in [TIME] + Resolved 10 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + attrs==23.2.0 + + outcome==1.3.0.post0 + + sortedcontainers==2.4.0 + + trio==0.25.0 "###); let pyproject_toml = context.read("pyproject.toml"); @@ -4132,7 +4141,7 @@ fn add_group() -> Result<()> { [dependency-groups] test = [ "anyio==3.7.0", - "trio", + "trio>=0.25.0", ] "### ); @@ -4144,8 +4153,8 @@ fn add_group() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Audited in [TIME] + Resolved 10 packages in [TIME] + Audited 3 packages in [TIME] "###); let pyproject_toml = context.read("pyproject.toml"); @@ -4164,7 +4173,7 @@ fn add_group() -> Result<()> { [dependency-groups] test = [ "anyio==3.7.0", - "trio", + "trio>=0.25.0", ] second = [ "anyio==3.7.0", diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index fb6cd32f352ac..4110122cef888 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -12319,7 +12319,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/snapshots/it__ecosystem__saleor-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__saleor-lock-file.snap index 0638e752324c0..8c79b4dce7ee8 100644 --- a/crates/uv/tests/it/snapshots/it__ecosystem__saleor-lock-file.snap +++ b/crates/uv/tests/it/snapshots/it__ecosystem__saleor-lock-file.snap @@ -1,5 +1,5 @@ --- -source: crates/uv/tests/ecosystem.rs +source: crates/uv/tests/it/ecosystem.rs expression: lock --- version = 1 @@ -1299,11 +1299,31 @@ source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/eb/9c097e058c9d5bb7cd39b32730397d645856a81360b4e49cafe16ec1f358/pillow-avif-plugin-1.4.6.tar.gz", hash = "sha256:855cf50d03f6fc16e1fd5e364b3cea0b79f4bf90d39ff2123969735d851e08ba", size = 19632 } wheels = [ { url = "https://files.pythonhosted.org/packages/b2/f7/460c854c3f4a9802aabd0a25b4814a7e5902c776a6501498a4078bf2a0d3/pillow_avif_plugin-1.4.6-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e2087daa49881421a5e703fcff80aa2cbcb5a455cf73114ed5f0ea2a697794c8", size = 7980980 }, + { url = "https://files.pythonhosted.org/packages/26/ce/4e84b4caf933c4e938076b238e57245157a501d9a990451024d91e3dae9d/pillow_avif_plugin-1.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc3256fd23f5c7bef4bcc562db9d2cd04634a7b01dee41ea35e8e92a2334a949", size = 8016324 }, { url = "https://files.pythonhosted.org/packages/f5/11/2f0fa7d135f91a8e34d9040b18a899d185776a642f5773ca33d45b0996ba/pillow_avif_plugin-1.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bacc0802516f054f98d9f218ada17b2e8a756e35cb71e7401bb8422848fe796", size = 5743257 }, + { url = "https://files.pythonhosted.org/packages/ab/11/d26281b45864aed5a157896ecc77d2d941d072ffac840d2a2c4a81b9f1a1/pillow_avif_plugin-1.4.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbd9a1a45d982e346063ec4f4ce100021c565ee3102f9ff7f678019d5febada8", size = 12296355 }, { url = "https://files.pythonhosted.org/packages/24/b6/5a2fda66a192c0a372bcd7968c5914ccc6dcd48cd57b2f6cccba4587e209/pillow_avif_plugin-1.4.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e74e53951228c3e6ff5141121bd2876e8aecdb27d5f12d01cc519258e0073d8b", size = 6431301 }, { url = "https://files.pythonhosted.org/packages/ac/1d/2d6f816e15e56b053758fbd6d625fbd79b5cf22e775fce9967b83ede8c31/pillow_avif_plugin-1.4.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37e1314500cec3457210f4c8a7583afe35751f076efa8122faa0f205403d645", size = 7984138 }, + { url = "https://files.pythonhosted.org/packages/0e/05/a3750549f914763d34c9a1dc1c4d78e2fe5021ad26faa71892f18c34b073/pillow_avif_plugin-1.4.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3008acd3edf86e8e2291d40e9f9eae86f5140415431d21f219df5ca8e8210115", size = 14667148 }, { url = "https://files.pythonhosted.org/packages/86/36/32e9576c512fb53096ee050a112a12c6054c4e9c6ce2ec9e7e6f4d9d5d11/pillow_avif_plugin-1.4.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d643db246d6c07994fbb98b5fa6c6ae8f9b19b4ed24566bc06942b7dad10ad47", size = 8123272 }, + { url = "https://files.pythonhosted.org/packages/73/39/8955b693f0983d4f9326dae2df3135bb455011d474baee6ba9273ccf56ac/pillow_avif_plugin-1.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d0d96859d1ebfd7c6c8daa761a05ee9df0a70278ec3011b3b5c6e56ac4996fa7", size = 12489803 }, + { url = "https://files.pythonhosted.org/packages/b2/0f/327be9b4aca874d7d8b05b4e3300e55b3123611ee26f6ec1f055d1cb0f74/pillow_avif_plugin-1.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d1654a1048ad09b6e7553d4eb6e6bd3848be512bab2e283275585609dbda8b0", size = 14911324 }, { url = "https://files.pythonhosted.org/packages/f0/5f/0bb9ec1910a5ece813ac6324b1d0f148cf71a0e5297ab8fcfce1e48a4ebe/pillow_avif_plugin-1.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:f262547edeec00ad287c8845ac6c9d7d822ef4b00d1832175c4c8fd692e34eba", size = 10564587 }, + { url = "https://files.pythonhosted.org/packages/3c/e2/eb6f75563b04188991bbe17d0c0350d9bb61886fa2660b749f34e555f3f2/pillow_avif_plugin-1.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d43a5de556e2ab8437e9d8b07da96e968e0498cb3c0d448c34198c7bac0387c", size = 8016288 }, + { url = "https://files.pythonhosted.org/packages/46/c3/675d2de2a68761f5125751f724217d6177ca6e8e78b889db6207441b1e3d/pillow_avif_plugin-1.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ce3fd54c76845c24dcd1cbc73fa5c72969df191bf7cd388a446f2f38342885c", size = 5676479 }, + { url = "https://files.pythonhosted.org/packages/21/33/7ee19bbbfb0568ea2f0dba644325e4111e65f7f7e99b9f1dcc956aa2d304/pillow_avif_plugin-1.4.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3c8d4f32a36ef4c345660a708067b6074dd380d0585589867333f7cb350bb9d", size = 12296248 }, + { url = "https://files.pythonhosted.org/packages/ce/40/51ba62d38a3ece90f5f22e641f0be952972a3c8238dd740e63c8d483dea3/pillow_avif_plugin-1.4.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f4563e5cd130016f8ea17602422b36abe4f63d2073ba98f2dbed42377b2f91c", size = 12214782 }, + { url = "https://files.pythonhosted.org/packages/bb/e2/7c6bce378c0d6c7a1a9f8a544e71e06a0a519c6d9ffc8e538dd015977a10/pillow_avif_plugin-1.4.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070b07e47012a3490ab56e62dd629cbe694240159df333f01692c3e5fb8acff8", size = 14394606 }, + { url = "https://files.pythonhosted.org/packages/31/33/a2f0785fcf20542fdfacea4390d3d1c5da26e7faeb10d9fb26cf374cd131/pillow_avif_plugin-1.4.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57e883963205b7cfe2981ca98db6554c488fbff2b5a6751cfcf065c6e657a922", size = 14666959 }, + { url = "https://files.pythonhosted.org/packages/cd/12/be3b001225d9ed4b2e3119682315b4a51bf3e3114a24664096b73bd31512/pillow_avif_plugin-1.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba1ae4dd323f019e0a1750555b02d91934724e4d556638baa60b5ca62e30f353", size = 12489897 }, + { url = "https://files.pythonhosted.org/packages/21/0a/6aa0f761308054b2aa7488ef9583c3125d1d2260b7d7482da0592a96ecaa/pillow_avif_plugin-1.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:73b840b612a6ce840e2206a9f097f4ad07c1ca4ed99a3b0d14444224daf55e88", size = 14911538 }, + { url = "https://files.pythonhosted.org/packages/19/80/3bdee7cd75ce7a1f20c2e1039d4a0e469185551032b95c68a58159b9fc9b/pillow_avif_plugin-1.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:a79fc89bae89be6bede9b4b01aff3967cd009c02edec1e70e6de8e52ef93b5fc", size = 10728675 }, + { url = "https://files.pythonhosted.org/packages/db/7e/fb4b950f16c3f43a030604a7f0672137c3c4a4b3a4fa98f3390dd88d0fa2/pillow_avif_plugin-1.4.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b1461731cde80ea246bce6ff87320367dbba206ee51a3370d99e679540dbcc17", size = 8016566 }, + { url = "https://files.pythonhosted.org/packages/1d/fb/0e3e2a252f2d92a8f453633144d35060d9bf53f5f5c7810f1fd0120bc1dd/pillow_avif_plugin-1.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fe32db84ba0c9d9b364e2b36a55620d6112133107f82854f19a4fdaa93fce66b", size = 5675393 }, + { url = "https://files.pythonhosted.org/packages/82/03/57b1c557ae05bf81cbcb2aa4cefd357861bdcfd1b055d53afd23d02902e8/pillow_avif_plugin-1.4.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ac4e238f172806b2f75a719895414ff8c67500ab0bfa691e53ee2a99cb85722", size = 14396630 }, + { url = "https://files.pythonhosted.org/packages/c2/00/12ec4d85453b85285b021e61bb027cadbf1bbb76b75d081ba0df7bc98d2a/pillow_avif_plugin-1.4.6-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38a3934bcce34eb1f457434b336e4df8da1cf1eaea9306ca9f12ab5fa466e5a9", size = 14671480 }, + { url = "https://files.pythonhosted.org/packages/9d/9e/c922cdc8168c526201e3927fee37605b2d0df93dc9e6b2586f65cd6fd35a/pillow_avif_plugin-1.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70b4e26bc604d7af87a5e5605b3480db9832eab0dbcf0b86565a813716653cce", size = 14914496 }, + { url = "https://files.pythonhosted.org/packages/30/1e/315c22079058ffc2b8c6028cb18e33f7dc420ec56c806e8428419347f427/pillow_avif_plugin-1.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:10e9b2ef297a9825b461715359ae233d6518d9863c877a8652c14d6acae6e9f0", size = 10729094 }, ] [[package]] diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 5bbb5a393cf29..9b7a99a274e91 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1012,6 +1012,86 @@ 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 = ["trio"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 11 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 11 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 11 packages in [TIME] + Prepared 4 packages in [TIME] + Uninstalled 2 packages in [TIME] + Installed 4 packages in [TIME] + - anyio==4.3.0 + + attrs==23.2.0 + + outcome==1.3.0.post0 + + sortedcontainers==2.4.0 + + trio==0.25.0 + - typing-extensions==4.10.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 11 packages in [TIME] + Installed 2 packages in [TIME] + + anyio==4.3.0 + + 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 8984ff239059a..02686b2b7eb3e 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1407,6 +1407,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.

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

@@ -1545,6 +1549,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.