mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
Validate dependency groups even when --frozen
is present (#11499)
## Summary We now use the same strategy as for extras, validating against the lockfile instead of the `pyproject.toml`. Closes https://github.com/astral-sh/uv/issues/10882.
This commit is contained in:
parent
71bda82b08
commit
f001605505
10 changed files with 168 additions and 161 deletions
|
@ -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`.
|
||||
|
|
|
@ -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};
|
||||
|
@ -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)]
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
@ -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())?,
|
||||
|
@ -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());
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
@ -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,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};
|
||||
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
@ -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())?;
|
||||
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
@ -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())?,
|
||||
|
@ -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();
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
@ -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())?,
|
||||
|
|
|
@ -1917,6 +1917,7 @@ fn export_group() -> Result<()> {
|
|||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["typing-extensions"]
|
||||
|
||||
[dependency-groups]
|
||||
foo = ["anyio ; sys_platform == 'darwin'"]
|
||||
bar = ["iniconfig"]
|
||||
|
@ -2055,6 +2056,16 @@ fn export_group() -> Result<()> {
|
|||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.export().arg("--all-groups").arg("--no-group").arg("baz"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
error: Group `baz` is not defined in the project's `dependency-groups` table
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -617,6 +617,7 @@ fn sync_legacy_non_project_group() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
error: Group `bop` is not defined in any project's `dependency-groups` table
|
||||
"###);
|
||||
|
||||
|
@ -1759,6 +1760,7 @@ fn sync_non_existent_group() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
error: Group `baz` is not defined in the project's `dependency-groups` table
|
||||
"###);
|
||||
|
||||
|
@ -1768,6 +1770,7 @@ fn sync_non_existent_group() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
error: Group `baz` is not defined in the project's `dependency-groups` table
|
||||
"###);
|
||||
|
||||
|
@ -1784,6 +1787,55 @@ fn sync_non_existent_group() -> Result<()> {
|
|||
+ typing-extensions==4.10.0
|
||||
"###);
|
||||
|
||||
// Requesting with `--frozen` should respect the groups in the lockfile, rather than the
|
||||
// `pyproject.toml`.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("bar"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Prepared 5 packages in [TIME]
|
||||
Installed 5 packages in [TIME]
|
||||
+ certifi==2024.2.2
|
||||
+ charset-normalizer==3.3.2
|
||||
+ idna==3.6
|
||||
+ requests==2.31.0
|
||||
+ urllib3==2.2.1
|
||||
"###);
|
||||
|
||||
// Replace `bar` with `baz`.
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["typing-extensions"]
|
||||
|
||||
[dependency-groups]
|
||||
baz = ["iniconfig"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("bar"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 6 packages in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--group").arg("baz"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Group `baz` is not defined in the project's `dependency-groups` table
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -5862,6 +5914,7 @@ fn sync_all_groups() -> Result<()> {
|
|||
[dependency-groups]
|
||||
types = ["sniffio>=1"]
|
||||
async = ["anyio>=3"]
|
||||
empty = []
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
|
@ -5944,9 +5997,22 @@ fn sync_all_groups() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 8 packages in [TIME]
|
||||
error: Group `foo` is not defined in any project's `dependency-groups` table
|
||||
"###);
|
||||
|
||||
// Sync an empty group.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("empty"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 8 packages in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
- packaging==24.0
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue