Add support for uv sync --all-packages (#8739)

## Summary

This PR enables `uv sync --all-packages` to sync all packages in a
workspace. It removes a common use-case for the legacy non-`[project]`
packages that we're trying to move away from.

Closes https://github.com/astral-sh/uv/issues/8724.
This commit is contained in:
Charlie Marsh 2024-11-01 21:55:08 -04:00 committed by GitHub
parent 58a9811881
commit 3c9dd97fe9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 407 additions and 83 deletions

View file

@ -2119,7 +2119,7 @@ pub struct BuildArgs {
/// directory if no source directory is provided.
///
/// If the workspace member does not exist, uv will exit with an error.
#[arg(long, conflicts_with("all"))]
#[arg(long, conflicts_with("all_packages"))]
pub package: Option<PackageName>,
/// Builds all packages in the workspace.
@ -2128,8 +2128,8 @@ pub struct BuildArgs {
/// directory if no source directory is provided.
///
/// If the workspace member does not exist, uv will exit with an error.
#[arg(long, conflicts_with("package"))]
pub all: bool,
#[arg(long, alias = "all", conflicts_with("package"))]
pub all_packages: bool,
/// The output directory to which distributions should be written.
///
@ -2912,13 +2912,23 @@ pub struct SyncArgs {
#[command(flatten)]
pub refresh: RefreshArgs,
/// Sync all packages in the workspace.
///
/// The workspace's environment (`.venv`) is updated to include all workspace
/// members.
///
/// Any extras or groups specified via `--extra`, `--group`, or related options
/// will be applied to all workspace members.
#[arg(long, conflicts_with = "package")]
pub all_packages: bool,
/// 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.
///
/// If the workspace member does not exist, uv will exit with an error.
#[arg(long)]
#[arg(long, conflicts_with = "all_packages")]
pub package: Option<PackageName>,
/// The Python interpreter to use for the project environment.

View file

@ -576,7 +576,7 @@ impl Lock {
/// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root.
pub fn to_resolution(
&self,
project: InstallTarget<'_>,
target: InstallTarget<'_>,
marker_env: &ResolverMarkerEnvironment,
tags: &Tags,
extras: &ExtrasSpecification,
@ -588,7 +588,7 @@ impl Lock {
let mut seen = FxHashSet::default();
// Add the workspace packages to the queue.
for root_name in project.packages() {
for root_name in target.packages() {
let root = self
.find_by_name(root_name)
.map_err(|_| LockErrorKind::MultipleRootPackages {
@ -638,7 +638,7 @@ impl Lock {
// Add any dependency groups that are exclusive to the workspace root (e.g., dev
// dependencies in (legacy) non-project workspace roots).
let groups = project
let groups = target
.groups()
.map_err(|err| LockErrorKind::DependencyGroup { err })?;
for group in dev.iter() {
@ -688,13 +688,13 @@ impl Lock {
}
if install_options.include_package(
&dist.id.name,
project.project_name(),
target.project_name(),
&self.manifest.members,
) {
map.insert(
dist.id.name.clone(),
ResolvedDist::Installable(dist.to_dist(
project.workspace().install_path(),
target.workspace().install_path(),
TagPolicy::Required(tags),
build_options,
)?),

View file

@ -1496,35 +1496,39 @@ impl VirtualProject {
/// A target that can be installed.
#[derive(Debug, Copy, Clone)]
pub enum InstallTarget<'env> {
/// An entire workspace.
Workspace(&'env Workspace),
/// A (legacy) non-project workspace root.
NonProjectWorkspace(&'env Workspace),
/// A project (which could be a workspace root or member).
Project(&'env ProjectWorkspace),
/// A (legacy) non-project workspace root.
NonProject(&'env Workspace),
/// A frozen member within a [`Workspace`].
FrozenMember(&'env Workspace, &'env PackageName),
FrozenProject(&'env Workspace, &'env PackageName),
}
impl<'env> InstallTarget<'env> {
/// Create an [`InstallTarget`] for a frozen member within a workspace.
pub fn frozen_member(project: &'env VirtualProject, package_name: &'env PackageName) -> Self {
Self::FrozenMember(project.workspace(), package_name)
pub fn frozen(project: &'env VirtualProject, package_name: &'env PackageName) -> Self {
Self::FrozenProject(project.workspace(), package_name)
}
/// Return the [`Workspace`] of the target.
pub fn workspace(&self) -> &Workspace {
match self {
Self::Workspace(workspace) => workspace,
Self::Project(project) => project.workspace(),
Self::NonProject(workspace) => workspace,
Self::FrozenMember(workspace, _) => workspace,
Self::NonProjectWorkspace(workspace) => workspace,
Self::FrozenProject(workspace, _) => workspace,
}
}
/// Return the [`PackageName`] of the target.
pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
match self {
Self::Workspace(workspace) => Either::Right(workspace.packages().keys()),
Self::Project(project) => Either::Left(std::iter::once(project.project_name())),
Self::NonProject(workspace) => Either::Right(workspace.packages().keys()),
Self::FrozenMember(_, package_name) => Either::Left(std::iter::once(*package_name)),
Self::NonProjectWorkspace(workspace) => Either::Right(workspace.packages().keys()),
Self::FrozenProject(_, package_name) => Either::Left(std::iter::once(*package_name)),
}
}
@ -1540,8 +1544,8 @@ impl<'env> InstallTarget<'env> {
DependencyGroupError,
> {
match self {
Self::Project(_) | Self::FrozenMember(..) => Ok(BTreeMap::new()),
Self::NonProject(workspace) => {
Self::Workspace(_) | Self::Project(_) | Self::FrozenProject(..) => Ok(BTreeMap::new()),
Self::NonProjectWorkspace(workspace) => {
// For non-projects, we might have `dependency-groups` or `tool.uv.dev-dependencies`
// that are attached to the workspace root (which isn't a member).
@ -1591,18 +1595,24 @@ impl<'env> InstallTarget<'env> {
/// Return the [`PackageName`] of the target, if available.
pub fn project_name(&self) -> Option<&PackageName> {
match self {
Self::Workspace(_) => None,
Self::Project(project) => Some(project.project_name()),
Self::NonProject(_) => None,
Self::FrozenMember(_, package_name) => Some(package_name),
}
Self::NonProjectWorkspace(_) => None,
Self::FrozenProject(_, package_name) => Some(package_name),
}
}
impl<'env> From<&'env VirtualProject> for InstallTarget<'env> {
fn from(project: &'env VirtualProject) -> Self {
pub fn from_workspace(workspace: &'env VirtualProject) -> Self {
match workspace {
VirtualProject::Project(project) => Self::Workspace(project.workspace()),
VirtualProject::NonProject(workspace) => Self::NonProjectWorkspace(workspace),
}
}
pub fn from_project(project: &'env VirtualProject) -> Self {
match project {
VirtualProject::Project(project) => Self::Project(project),
VirtualProject::NonProject(workspace) => Self::NonProject(workspace),
VirtualProject::NonProject(workspace) => Self::NonProjectWorkspace(workspace),
}
}
}

View file

@ -43,7 +43,7 @@ pub(crate) async fn build_frontend(
project_dir: &Path,
src: Option<PathBuf>,
package: Option<PackageName>,
all: bool,
all_packages: bool,
output_dir: Option<PathBuf>,
sdist: bool,
wheel: bool,
@ -65,7 +65,7 @@ pub(crate) async fn build_frontend(
project_dir,
src.as_deref(),
package.as_ref(),
all,
all_packages,
output_dir.as_deref(),
sdist,
wheel,
@ -105,7 +105,7 @@ async fn build_impl(
project_dir: &Path,
src: Option<&Path>,
package: Option<&PackageName>,
all: bool,
all_packages: bool,
output_dir: Option<&Path>,
sdist: bool,
wheel: bool,
@ -171,7 +171,7 @@ async fn build_impl(
// Attempt to discover the workspace; on failure, save the error for later.
let workspace = Workspace::discover(src.directory(), &DiscoveryOptions::default()).await;
// If a `--package` or `--all` was provided, adjust the source directory.
// If a `--package` or `--all-packages` was provided, adjust the source directory.
let packages = if let Some(package) = package {
if matches!(src, Source::File(_)) {
return Err(anyhow::anyhow!(
@ -201,10 +201,10 @@ async fn build_impl(
vec![AnnotatedSource::from(Source::Directory(Cow::Borrowed(
package.root(),
)))]
} else if all {
} else if all_packages {
if matches!(src, Source::File(_)) {
return Err(anyhow::anyhow!(
"Cannot specify `--all` when building from a file"
"Cannot specify `--all-packages` when building from a file"
));
}
@ -212,7 +212,7 @@ async fn build_impl(
Ok(ref workspace) => workspace,
Err(err) => {
return Err(anyhow::Error::from(err)
.context("`--all` was provided, but no workspace was found"));
.context("`--all-packages` was provided, but no workspace was found"));
}
};

View file

@ -870,7 +870,7 @@ async fn lock_and_sync(
};
project::sync::do_sync(
InstallTarget::from(&project),
InstallTarget::from_project(&project),
venv,
&lock,
&extras,

View file

@ -14,7 +14,7 @@ use uv_configuration::{
use uv_normalize::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_resolver::RequirementsTxtExport;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace};
use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace};
use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::project::lock::{do_safe_lock, LockMode};
@ -73,7 +73,7 @@ pub(crate) async fn export(
};
// Determine the default groups to include.
validate_dependency_groups(&project, &dev)?;
validate_dependency_groups(InstallTarget::from_project(&project), &dev)?;
let defaults = default_dependency_groups(project.pyproject_toml())?;
let VirtualProject::Project(project) = project else {

View file

@ -38,7 +38,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::pyproject::PyProjectToml;
use uv_workspace::{VirtualProject, Workspace};
use uv_workspace::{InstallTarget, Workspace};
use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::pip::operations::{Changelog, Modifications};
@ -1369,7 +1369,7 @@ pub(crate) async fn script_python_requirement(
/// Validate the dependency groups requested by the [`DevGroupsSpecification`].
#[allow(clippy::result_large_err)]
pub(crate) fn validate_dependency_groups(
project: &VirtualProject,
target: InstallTarget<'_>,
dev: &DevGroupsSpecification,
) -> Result<(), ProjectError> {
for group in dev
@ -1377,8 +1377,14 @@ pub(crate) fn validate_dependency_groups(
.into_iter()
.flat_map(GroupsSpecification::names)
{
match project {
VirtualProject::Project(project) => {
match target {
InstallTarget::Workspace(workspace) | InstallTarget::NonProjectWorkspace(workspace) => {
// The group must be defined in the workspace.
if !workspace.groups().contains(group) {
return Err(ProjectError::MissingGroupWorkspace(group.clone()));
}
}
InstallTarget::Project(project) => {
// The group must be defined in the target project.
if !project
.current_project()
@ -1390,25 +1396,7 @@ pub(crate) fn validate_dependency_groups(
return Err(ProjectError::MissingGroupProject(group.clone()));
}
}
VirtualProject::NonProject(workspace) => {
// The group must be defined in at least one workspace package.
if !workspace
.pyproject_toml()
.dependency_groups
.as_ref()
.is_some_and(|groups| groups.contains_key(group))
{
if workspace.packages().values().all(|package| {
!package
.pyproject_toml()
.dependency_groups
.as_ref()
.is_some_and(|groups| groups.contains_key(group))
}) {
return Err(ProjectError::MissingGroupWorkspace(group.clone()));
}
}
}
InstallTarget::FrozenProject(_, _) => {}
}
}
Ok(())

View file

@ -236,7 +236,7 @@ pub(crate) async fn remove(
let defaults = default_dependency_groups(project.pyproject_toml())?;
project::sync::do_sync(
InstallTarget::from(&project),
InstallTarget::from_project(&project),
&venv,
&lock,
&extras,

View file

@ -551,7 +551,7 @@ pub(crate) async fn run(
}
} else {
// Determine the default groups to include.
validate_dependency_groups(&project, &dev)?;
validate_dependency_groups(InstallTarget::from_project(&project), &dev)?;
let defaults = default_dependency_groups(project.pyproject_toml())?;
// Determine the lock mode.
@ -607,7 +607,7 @@ pub(crate) async fn run(
let install_options = InstallOptions::default();
project::sync::do_sync(
InstallTarget::from(&project),
InstallTarget::from_project(&project),
&venv,
result.lock(),
&extras,

View file

@ -33,7 +33,7 @@ use crate::commands::project::lock::{do_safe_lock, LockMode};
use crate::commands::project::{
default_dependency_groups, validate_dependency_groups, ProjectError, SharedState,
};
use crate::commands::{diagnostics, pip, project, ExitStatus};
use crate::commands::{diagnostics, project, ExitStatus};
use crate::printer::Printer;
use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings};
@ -43,6 +43,7 @@ pub(crate) async fn sync(
project_dir: &Path,
locked: bool,
frozen: bool,
all_packages: bool,
package: Option<PackageName>,
extras: ExtrasSpecification,
dev: DevGroupsSpecification,
@ -60,7 +61,7 @@ pub(crate) async fn sync(
printer: Printer,
) -> Result<ExitStatus> {
// Identify the project.
let project = if frozen {
let project = if frozen && !all_packages {
VirtualProject::discover(
project_dir,
&DiscoveryOptions {
@ -82,9 +83,11 @@ pub(crate) async fn sync(
// Identify the target.
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
InstallTarget::frozen_member(&project, package)
InstallTarget::frozen(&project, package)
} else if all_packages {
InstallTarget::from_workspace(&project)
} else {
InstallTarget::from(&project)
InstallTarget::from_project(&project)
};
// TODO(lucab): improve warning content
@ -96,7 +99,7 @@ pub(crate) async fn sync(
}
// Determine the default groups to include.
validate_dependency_groups(&project, &dev)?;
validate_dependency_groups(target, &dev)?;
let defaults = default_dependency_groups(project.pyproject_toml())?;
// Discover or create the virtual environment.
@ -363,7 +366,7 @@ pub(super) async fn do_sync(
let site_packages = SitePackages::from_environment(venv)?;
// Sync the environment.
pip::operations::install(
operations::install(
&resolution,
site_packages,
modifications,

View file

@ -9,7 +9,7 @@ use uv_configuration::{Concurrency, DevGroupsSpecification, LowerBound, TargetTr
use uv_pep508::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion};
use uv_resolver::TreeDisplay;
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace};
use uv_workspace::{DiscoveryOptions, InstallTarget, Workspace};
use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::resolution_markers;
@ -50,7 +50,7 @@ pub(crate) async fn tree(
let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;
// Determine the default groups to include.
validate_dependency_groups(&VirtualProject::NonProject(workspace.clone()), &dev)?;
validate_dependency_groups(InstallTarget::Workspace(&workspace), &dev)?;
let defaults = default_dependency_groups(workspace.pyproject_toml())?;
// Find an interpreter for the project, unless `--frozen` and `--universal` are both set.

View file

@ -712,7 +712,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
&project_dir,
args.src,
args.package,
args.all,
args.all_packages,
args.out_dir,
args.sdist,
args.wheel,
@ -1327,6 +1327,7 @@ async fn run_project(
project_dir,
args.locked,
args.frozen,
args.all_packages,
args.package,
args.extras,
args.dev,

View file

@ -720,6 +720,7 @@ pub(crate) struct SyncSettings {
pub(crate) editable: EditableMode,
pub(crate) install_options: InstallOptions,
pub(crate) modifications: Modifications,
pub(crate) all_packages: bool,
pub(crate) package: Option<PackageName>,
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
@ -751,6 +752,7 @@ impl SyncSettings {
installer,
build,
refresh,
all_packages,
package,
python,
} = args;
@ -781,6 +783,7 @@ impl SyncSettings {
} else {
Modifications::Sufficient
},
all_packages,
package,
python: python.and_then(Maybe::into_option),
refresh: Refresh::from(refresh),
@ -1797,7 +1800,7 @@ impl PipCheckSettings {
pub(crate) struct BuildSettings {
pub(crate) src: Option<PathBuf>,
pub(crate) package: Option<PackageName>,
pub(crate) all: bool,
pub(crate) all_packages: bool,
pub(crate) out_dir: Option<PathBuf>,
pub(crate) sdist: bool,
pub(crate) wheel: bool,
@ -1816,7 +1819,7 @@ impl BuildSettings {
src,
out_dir,
package,
all,
all_packages,
sdist,
wheel,
build_constraint,
@ -1835,7 +1838,7 @@ impl BuildSettings {
Self {
src,
package,
all,
all_packages,
out_dir,
sdist,
wheel,

View file

@ -1206,7 +1206,7 @@ fn workspace() -> Result<()> {
----- stdout -----
----- stderr -----
error: `--all` was provided, but no workspace was found
error: `--all-packages` was provided, but no workspace was found
Caused by: No `pyproject.toml` found in current directory or any parent directory
"###);

View file

@ -263,7 +263,7 @@ fn package() -> Result<()> {
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>1"]
dependencies = ["iniconfig>=1"]
[build-system]
requires = ["setuptools>=42"]
@ -415,7 +415,7 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> {
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>1"]
dependencies = ["iniconfig>=1"]
[build-system]
requires = ["setuptools>=42"]
@ -500,7 +500,7 @@ fn sync_legacy_non_project_group() -> Result<()> {
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>1"]
dependencies = ["iniconfig>=1"]
[dependency-groups]
baz = ["typing-extensions"]
@ -1805,7 +1805,7 @@ fn no_install_workspace() -> Result<()> {
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>1"]
dependencies = ["iniconfig>=1"]
[build-system]
requires = ["setuptools>=42"]
@ -3073,7 +3073,7 @@ fn transitive_dev() -> Result<()> {
build-backend = "setuptools.build_meta"
[tool.uv]
dev-dependencies = ["iniconfig>1"]
dev-dependencies = ["iniconfig>=1"]
"#,
)?;
@ -3425,7 +3425,7 @@ fn build_system_requires_workspace() -> Result<()> {
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>1"]
dependencies = ["iniconfig>=1"]
[build-system]
requires = ["setuptools>=42", "backend==0.1.0"]
@ -3508,7 +3508,7 @@ fn build_system_requires_path() -> Result<()> {
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>1"]
dependencies = ["iniconfig>=1"]
[build-system]
requires = ["setuptools>=42", "backend==0.1.0"]
@ -3798,3 +3798,306 @@ fn sync_explicit() -> Result<()> {
Ok(())
}
/// Sync all members in a workspace.
#[test]
fn sync_all() -> 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 = ["anyio>3", "child"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.uv.workspace]
members = ["child"]
[tool.uv.sources]
child = { workspace = true }
"#,
)?;
context
.temp_dir
.child("src")
.child("project")
.child("__init__.py")
.touch()?;
// Add a workspace member.
let child = context.temp_dir.child("child");
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=1"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// Generate a lockfile.
context.lock().assert().success();
// Sync all workspace members.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 6 packages in [TIME]
Installed 6 packages in [TIME]
+ anyio==4.3.0
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ idna==3.6
+ iniconfig==2.0.0
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
Ok(())
}
/// Sync all members in a workspace with extras attached.
#[test]
fn sync_all_extras() -> 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 = ["child"]
[project.optional-dependencies]
types = ["sniffio>1"]
async = ["anyio>3"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.uv.workspace]
members = ["child"]
[tool.uv.sources]
child = { workspace = true }
"#,
)?;
context
.temp_dir
.child("src")
.child("project")
.child("__init__.py")
.touch()?;
// Add a workspace member.
let child = context.temp_dir.child("child");
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=1"]
[project.optional-dependencies]
types = ["typing-extensions>=4"]
testing = ["packaging>=24"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// Generate a lockfile.
context.lock().assert().success();
// Sync an extra that exists in both the parent and child.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("types"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ iniconfig==2.0.0
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
// Sync an extra that only exists in the child.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("testing"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 2 packages in [TIME]
Installed 1 package in [TIME]
+ packaging==24.0
- sniffio==1.3.1
- typing-extensions==4.10.0
"###);
// Sync all extras.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
Ok(())
}
/// Sync all members in a workspace with dependency groups attached.
#[test]
fn sync_all_groups() -> 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 = ["child"]
[dependency-groups]
types = ["sniffio>=1"]
async = ["anyio>=3"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.uv.workspace]
members = ["child"]
[tool.uv.sources]
child = { workspace = true }
"#,
)?;
context
.temp_dir
.child("src")
.child("project")
.child("__init__.py")
.touch()?;
// Add a workspace member.
let child = context.temp_dir.child("child");
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=1"]
[dependency-groups]
types = ["typing-extensions>=4"]
testing = ["packaging>=24"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// Generate a lockfile.
context.lock().assert().success();
// Sync a group that exists in both the parent and child.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("types"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ iniconfig==2.0.0
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
// Sync a group that only exists in the child.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("testing"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 2 packages in [TIME]
Installed 1 package in [TIME]
+ packaging==24.0
- sniffio==1.3.1
- typing-extensions==4.10.0
"###);
// Sync a group that doesn't exist.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("foo"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Group `foo` is not defined in any project's `dependency-group` table
"###);
Ok(())
}

View file

@ -1369,6 +1369,12 @@ uv sync [OPTIONS]
<p>Note that all optional dependencies are always included in the resolution; this option only affects the selection of packages to install.</p>
</dd><dt><code>--all-packages</code></dt><dd><p>Sync all packages in the workspace.</p>
<p>The workspace&#8217;s environment (<code>.venv</code>) is updated to include all workspace members.</p>
<p>Any extras or groups specified via <code>--extra</code>, <code>--group</code>, or related options will be applied to all workspace members.</p>
</dd><dt><code>--allow-insecure-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p>
<p>Can be provided multiple times.</p>
@ -7260,7 +7266,7 @@ uv build [OPTIONS] [SRC]
<h3 class="cli-reference">Options</h3>
<dl class="cli-reference"><dt><code>--all</code></dt><dd><p>Builds all packages in the workspace.</p>
<dl class="cli-reference"><dt><code>--all-packages</code></dt><dd><p>Builds all packages in the workspace.</p>
<p>The workspace will be discovered from the provided source directory, or the current directory if no source directory is provided.</p>