Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2622,6 +2622,11 @@ impl Package {
pub fn provides_extras(&self) -> Option<&Vec<ExtraName>> {
self.metadata.provides_extras.as_ref()
}

/// Returns the dependency groups the package provides, if any.
pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
&self.metadata.dependency_groups
}
}

/// Attempts to construct a `VerbatimUrl` from the given normalized `Path`.
Expand Down
43 changes: 2 additions & 41 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Resolve the current [`ProjectWorkspace`] or [`Workspace`].

use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use glob::{glob, GlobError, PatternError};
Expand All @@ -19,8 +19,7 @@ use uv_warnings::warn_user_once;

use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroups};
use crate::pyproject::{
DependencyGroups, Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources,
ToolUvWorkspace,
Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace,
};

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -488,44 +487,6 @@ impl Workspace {
constraints.clone()
}

/// Returns the set of all dependency group names defined in the workspace.
pub fn groups(&self) -> BTreeSet<&GroupName> {
self.pyproject_toml
.dependency_groups
.iter()
.flat_map(DependencyGroups::keys)
.chain(
self.packages
.values()
.filter_map(|member| member.pyproject_toml.dependency_groups.as_ref())
.flat_map(DependencyGroups::keys),
)
.chain(
if self
.pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.dev_dependencies.as_ref())
.is_some()
|| self.packages.values().any(|member| {
member
.pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.dev_dependencies.as_ref())
.is_some()
})
{
Some(&*DEV_DEPENDENCIES)
} else {
None
},
)
.collect()
}

/// The path to the workspace root, the directory containing the top level `pyproject.toml` with
/// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project.
pub fn install_path(&self) -> &PathBuf {
Expand Down
26 changes: 6 additions & 20 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::{do_safe_lock, LockMode};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, detect_conflicts, DependencyGroupsTarget, ProjectError,
ProjectInterpreter, ScriptInterpreter, UniversalState,
default_dependency_groups, detect_conflicts, ProjectError, ProjectInterpreter,
ScriptInterpreter, UniversalState,
};
use crate::commands::{diagnostics, ExitStatus, OutputWriter};
use crate::printer::Printer;
Expand Down Expand Up @@ -107,24 +107,6 @@ pub(crate) async fn export(
ExportTarget::Project(project)
};

// Validate that any referenced dependency groups are defined in the workspace.
if !frozen {
let target = match &target {
ExportTarget::Project(VirtualProject::Project(project)) => {
if all_packages {
DependencyGroupsTarget::Workspace(project.workspace())
} else {
DependencyGroupsTarget::Project(project)
}
}
ExportTarget::Project(VirtualProject::NonProject(workspace)) => {
DependencyGroupsTarget::Workspace(workspace)
}
ExportTarget::Script(_) => DependencyGroupsTarget::Script,
};
target.validate(&dev)?;
}

// Determine the default groups to include.
let defaults = match &target {
ExportTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?,
Expand Down Expand Up @@ -268,6 +250,10 @@ pub(crate) async fn export(
},
};

// Validate that the set of requested extras and development groups are defined in the lockfile.
target.validate_extras(&extras)?;
target.validate_groups(&dev)?;

// Write the resolved dependencies to the output channel.
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());

Expand Down
64 changes: 63 additions & 1 deletion crates/uv/src/commands/project/install_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::str::FromStr;
use itertools::Either;
use rustc_hash::FxHashSet;

use uv_configuration::ExtrasSpecification;
use uv_configuration::{DevGroupsManifest, ExtrasSpecification};
use uv_distribution_types::Index;
use uv_normalize::PackageName;
use uv_pypi_types::{LenientRequirement, VerbatimParsedUrl};
Expand Down Expand Up @@ -302,4 +302,66 @@ impl<'lock> InstallTarget<'lock> {

Ok(())
}

/// Validate the dependency groups requested by the [`DependencyGroupSpecifier`].
#[allow(clippy::result_large_err)]
pub(crate) fn validate_groups(self, groups: &DevGroupsManifest) -> Result<(), ProjectError> {
// If no groups were specified, short-circuit.
if groups.explicit_names().next().is_none() {
return Ok(());
}

match self {
Self::Workspace { lock, workspace } | Self::NonProjectWorkspace { lock, workspace } => {
let roots = self.roots().collect::<FxHashSet<_>>();
let member_packages: Vec<&Package> = lock
.packages()
.iter()
.filter(|package| roots.contains(package.name()))
.collect();

// Extract the dependency groups that are exclusive to the workspace root.
let known_groups = member_packages
.iter()
.flat_map(|package| package.dependency_groups().keys().map(Cow::Borrowed))
.chain(workspace.dependency_groups().ok().into_iter().flat_map(
|dependency_groups| dependency_groups.into_keys().map(Cow::Owned),
))
.collect::<FxHashSet<_>>();

for group in groups.explicit_names() {
if !known_groups.contains(group) {
return Err(ProjectError::MissingGroupWorkspace(group.clone()));
}
}
}
Self::Project { lock, .. } => {
let roots = self.roots().collect::<FxHashSet<_>>();
let member_packages: Vec<&Package> = lock
.packages()
.iter()
.filter(|package| roots.contains(package.name()))
.collect();

// Extract the dependency groups defined in the relevant member.
let known_groups = member_packages
.iter()
.flat_map(|package| package.dependency_groups().keys())
.collect::<FxHashSet<_>>();

for group in groups.explicit_names() {
if !known_groups.contains(group) {
return Err(ProjectError::MissingGroupProject(group.clone()));
}
}
}
Self::Script { .. } => {
if let Some(group) = groups.explicit_names().next() {
return Err(ProjectError::MissingGroupScript(group.clone()));
}
}
}

Ok(())
}
}
45 changes: 1 addition & 44 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,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::{ProjectWorkspace, Workspace};
use uv_workspace::Workspace;

use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::pip::operations::{Changelog, Modifications};
Expand Down Expand Up @@ -2261,49 +2261,6 @@ pub(crate) async fn init_script_python_requirement(
))
}

#[derive(Debug, Copy, Clone)]
pub(crate) enum DependencyGroupsTarget<'env> {
/// The dependency groups can be defined in any workspace member.
Workspace(&'env Workspace),
/// The dependency groups must be defined in the target project.
Project(&'env ProjectWorkspace),
/// The dependency groups must be defined in the target script.
Script,
}

impl DependencyGroupsTarget<'_> {
/// Validate the dependency groups requested by the [`DevGroupsSpecification`].
#[allow(clippy::result_large_err)]
pub(crate) fn validate(self, dev: &DevGroupsSpecification) -> Result<(), ProjectError> {
for group in dev.explicit_names() {
match self {
Self::Workspace(workspace) => {
// The group must be defined in the workspace.
if !workspace.groups().contains(group) {
return Err(ProjectError::MissingGroupWorkspace(group.clone()));
}
}
Self::Project(project) => {
// The group must be defined in the target project.
if !project
.current_project()
.pyproject_toml()
.dependency_groups
.as_ref()
.is_some_and(|groups| groups.contains_key(group))
{
return Err(ProjectError::MissingGroupProject(group.clone()));
}
}
Self::Script => {
return Err(ProjectError::MissingGroupScript(group.clone()));
}
}
}
Ok(())
}
}

/// Returns the default dependency groups from the [`PyProjectToml`].
#[allow(clippy::result_large_err)]
pub(crate) fn default_dependency_groups(
Expand Down
27 changes: 8 additions & 19 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ use crate::commands::project::lock::LockMode;
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, script_specification, update_environment,
validate_project_requires_python, DependencyGroupsTarget, EnvironmentSpecification,
ProjectEnvironment, ProjectError, ScriptEnvironment, ScriptInterpreter, UniversalState,
WorkspacePython,
validate_project_requires_python, EnvironmentSpecification, ProjectEnvironment, ProjectError,
ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython,
};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::run::run_to_completion;
Expand Down Expand Up @@ -648,21 +647,6 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
}
} else {
// Validate that any referenced dependency groups are defined in the workspace.
if !frozen {
let target = match &project {
VirtualProject::Project(project) => {
if all_packages {
DependencyGroupsTarget::Workspace(project.workspace())
} else {
DependencyGroupsTarget::Project(project)
}
}
VirtualProject::NonProject(workspace) => {
DependencyGroupsTarget::Workspace(workspace)
}
};
target.validate(&dev)?;
}

// Determine the default groups to include.
let defaults = default_dependency_groups(project.pyproject_toml())?;
Expand Down Expand Up @@ -751,12 +735,17 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
};

let install_options = InstallOptions::default();
let dev = dev.with_defaults(defaults);

// Validate that the set of requested extras and development groups are defined in the lockfile.
target.validate_extras(&extras)?;
target.validate_groups(&dev)?;

match project::sync::do_sync(
target,
&venv,
&extras,
&dev.with_defaults(defaults),
&dev,
editable,
install_options,
modifications,
Expand Down
30 changes: 5 additions & 25 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ use crate::commands::project::lock::{do_safe_lock, LockMode, LockResult};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, detect_conflicts, script_specification, update_environment,
DependencyGroupsTarget, PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment,
UniversalState,
PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment, UniversalState,
};
use crate::commands::{diagnostics, ExitStatus};
use crate::printer::Printer;
Expand Down Expand Up @@ -113,28 +112,6 @@ pub(crate) async fn sync(
SyncTarget::Project(project)
};

// Validate that any referenced dependency groups are defined in the workspace.
if !frozen {
match &target {
SyncTarget::Project(project) => {
let target = match &project {
VirtualProject::Project(project) => {
if all_packages {
DependencyGroupsTarget::Workspace(project.workspace())
} else {
DependencyGroupsTarget::Project(project)
}
}
VirtualProject::NonProject(workspace) => {
DependencyGroupsTarget::Workspace(workspace)
}
};
target.validate(&dev)?;
}
SyncTarget::Script(..) => {}
}
}

// Determine the default groups to include.
let defaults = match &target {
SyncTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?,
Expand Down Expand Up @@ -587,9 +564,12 @@ pub(super) async fn do_sync(
}

// Validate that the set of requested extras and development groups are compatible.
target.validate_extras(extras)?;
detect_conflicts(target.lock(), extras, dev)?;

// Validate that the set of requested extras and development groups are defined in the lockfile.
target.validate_extras(extras)?;
target.validate_groups(dev)?;

// Determine the markers to use for resolution.
let marker_env = venv.interpreter().resolver_marker_environment();

Expand Down
12 changes: 1 addition & 11 deletions crates/uv/src/commands/project/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ use crate::commands::pip::resolution_markers;
use crate::commands::project::lock::{do_safe_lock, LockMode};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, DependencyGroupsTarget, ProjectError, ProjectInterpreter,
ScriptInterpreter, UniversalState,
default_dependency_groups, ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState,
};
use crate::commands::reporters::LatestVersionReporter;
use crate::commands::{diagnostics, ExitStatus};
Expand Down Expand Up @@ -72,15 +71,6 @@ pub(crate) async fn tree(
LockTarget::Workspace(&workspace)
};

// Validate that any referenced dependency groups are defined in the target.
if !frozen {
let target = match &target {
LockTarget::Workspace(workspace) => DependencyGroupsTarget::Workspace(workspace),
LockTarget::Script(..) => DependencyGroupsTarget::Script,
};
target.validate(&dev)?;
}

// Determine the default groups to include.
let defaults = match target {
LockTarget::Workspace(workspace) => default_dependency_groups(workspace.pyproject_toml())?,
Expand Down
Loading
Loading