Skip to content

Commit

Permalink
Respect frozen
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Feb 14, 2025
1 parent 71bda82 commit 5f4c80e
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 161 deletions.
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

0 comments on commit 5f4c80e

Please sign in to comment.