Skip to content

Commit

Permalink
Add support for uv sync --all
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Nov 1, 2024
1 parent 40b4826 commit 1e997d5
Show file tree
Hide file tree
Showing 14 changed files with 392 additions and 68 deletions.
12 changes: 11 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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")]
pub package: Option<PackageName>,

/// The Python interpreter to use for the project environment.
Expand Down
10 changes: 5 additions & 5 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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,
)?),
Expand Down
44 changes: 27 additions & 17 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
}
}

Expand All @@ -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).

Expand Down Expand Up @@ -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),
}
}

pub fn from_workspace(workspace: &'env VirtualProject) -> Self {
match workspace {
VirtualProject::Project(project) => Self::Workspace(project.workspace()),
VirtualProject::NonProject(workspace) => Self::NonProjectWorkspace(workspace),
}
}
}

impl<'env> From<&'env VirtualProject> for InstallTarget<'env> {
fn from(project: &'env VirtualProject) -> Self {
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),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ async fn lock_and_sync(
};

project::sync::do_sync(
InstallTarget::from(&project),
InstallTarget::from_project(&project),
venv,
&lock,
&extras,
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down
34 changes: 11 additions & 23 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -1369,16 +1369,22 @@ 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
.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()
Expand All @@ -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(())
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 9 additions & 6 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -43,6 +43,7 @@ pub(crate) async fn sync(
project_dir: &Path,
locked: bool,
frozen: bool,
all: bool,
package: Option<PackageName>,
extras: ExtrasSpecification,
dev: DevGroupsSpecification,
Expand All @@ -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 {
VirtualProject::discover(
project_dir,
&DiscoveryOptions {
Expand All @@ -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 {
InstallTarget::from_workspace(&project)
} else {
InstallTarget::from(&project)
InstallTarget::from_project(&project)
};

// TODO(lucab): improve warning content
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,7 @@ async fn run_project(
project_dir,
args.locked,
args.frozen,
args.all,
args.package,
args.extras,
args.dev,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ pub(crate) struct SyncSettings {
pub(crate) editable: EditableMode,
pub(crate) install_options: InstallOptions,
pub(crate) modifications: Modifications,
pub(crate) all: bool,
pub(crate) package: Option<PackageName>,
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
Expand Down Expand Up @@ -751,6 +752,7 @@ impl SyncSettings {
installer,
build,
refresh,
all,
package,
python,
} = args;
Expand Down Expand Up @@ -781,6 +783,7 @@ impl SyncSettings {
} else {
Modifications::Sufficient
},
all,
package,
python: python.and_then(Maybe::into_option),
refresh: Refresh::from(refresh),
Expand Down
Loading

0 comments on commit 1e997d5

Please sign in to comment.