Skip to content

Commit fdfd7d9

Browse files
committed
uv-workspace: remove package name from conflicting-groups schema
In order to support this, we define a nearly duplicative set of types just for `pyproject.toml` deserialization. We also permit the conflicts to be specified in workspace member `pyproject.toml` files. So now we collect all of them and merged them into one giant set to feed to the resolver. Note that this also removes support for specifying conflicts in `uv.toml`. I think I didn't mean to add that originally. We could add it back, but in that context, we would need the package name I believe.
1 parent 8f9a8f3 commit fdfd7d9

File tree

8 files changed

+189
-85
lines changed

8 files changed

+189
-85
lines changed

crates/uv-pypi-types/src/conflicting_groups.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ impl ConflictingGroupList {
3838
pub fn is_empty(&self) -> bool {
3939
self.0.is_empty()
4040
}
41+
42+
/// Appends the given list to this one. This drains all elements
43+
/// from the list given, such that after this call, it is empty.
44+
pub fn append(&mut self, other: &mut ConflictingGroupList) {
45+
self.0.append(&mut other.0);
46+
}
4147
}
4248

4349
/// A single set of package-extra pairs that conflict with one another.
@@ -193,3 +199,99 @@ pub enum ConflictingGroupError {
193199
#[error("Each set of conflicting groups must have at least two entries, but found only one")]
194200
OneGroup,
195201
}
202+
203+
/// Like [`ConflictingGroupList`], but for deserialization in `pyproject.toml`.
204+
///
205+
/// The schema format is different from the in-memory format. Specifically, the
206+
/// schema format does not allow specifying the package name (or will make it
207+
/// optional in the future), where as the in-memory format needs the package
208+
/// name.
209+
///
210+
/// N.B. `ConflictingGroupList` is still used for (de)serialization.
211+
/// Specifically, in the lock file, where the package name is required.
212+
#[derive(
213+
Debug, Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
214+
)]
215+
pub struct SchemaConflictingGroupList(Vec<SchemaConflictingGroups>);
216+
217+
impl SchemaConflictingGroupList {
218+
/// Convert the public schema "conflicting" type to our internal
219+
/// fully resolved type. Effectively, this pairs the corresponding
220+
/// package name with each conflict.
221+
pub fn to_conflicting_with_package_name(&self, package: &PackageName) -> ConflictingGroupList {
222+
let mut conflicting = ConflictingGroupList::empty();
223+
for tool_uv_set in &self.0 {
224+
let mut set = vec![];
225+
for item in &tool_uv_set.0 {
226+
set.push(ConflictingGroup::from((
227+
package.clone(),
228+
item.extra.clone(),
229+
)));
230+
}
231+
// OK because we guarantee that
232+
// `SchemaConflictingGroupList` is valid and there aren't
233+
// any new errors that can occur here.
234+
let set = ConflictingGroups::try_from(set).unwrap();
235+
conflicting.push(set);
236+
}
237+
conflicting
238+
}
239+
}
240+
241+
/// Like [`ConflictingGroups`], but for deserialization in `pyproject.toml`.
242+
///
243+
/// The schema format is different from the in-memory format. Specifically, the
244+
/// schema format does not allow specifying the package name (or will make it
245+
/// optional in the future), where as the in-memory format needs the package
246+
/// name.
247+
#[derive(Debug, Default, Clone, Eq, PartialEq, serde::Serialize, schemars::JsonSchema)]
248+
pub struct SchemaConflictingGroups(Vec<SchemaConflictingGroup>);
249+
250+
/// Like [`ConflictingGroup`], but for deserialization in `pyproject.toml`.
251+
///
252+
/// The schema format is different from the in-memory format. Specifically, the
253+
/// schema format does not allow specifying the package name (or will make it
254+
/// optional in the future), where as the in-memory format needs the package
255+
/// name.
256+
#[derive(
257+
Debug,
258+
Default,
259+
Clone,
260+
Eq,
261+
Hash,
262+
PartialEq,
263+
PartialOrd,
264+
Ord,
265+
serde::Deserialize,
266+
serde::Serialize,
267+
schemars::JsonSchema,
268+
)]
269+
#[serde(deny_unknown_fields)]
270+
pub struct SchemaConflictingGroup {
271+
extra: ExtraName,
272+
}
273+
274+
impl<'de> serde::Deserialize<'de> for SchemaConflictingGroups {
275+
fn deserialize<D>(deserializer: D) -> Result<SchemaConflictingGroups, D::Error>
276+
where
277+
D: serde::Deserializer<'de>,
278+
{
279+
let items = Vec::<SchemaConflictingGroup>::deserialize(deserializer)?;
280+
Self::try_from(items).map_err(serde::de::Error::custom)
281+
}
282+
}
283+
284+
impl TryFrom<Vec<SchemaConflictingGroup>> for SchemaConflictingGroups {
285+
type Error = ConflictingGroupError;
286+
287+
fn try_from(
288+
items: Vec<SchemaConflictingGroup>,
289+
) -> Result<SchemaConflictingGroups, ConflictingGroupError> {
290+
match items.len() {
291+
0 => return Err(ConflictingGroupError::ZeroGroups),
292+
1 => return Err(ConflictingGroupError::OneGroup),
293+
_ => {}
294+
}
295+
Ok(SchemaConflictingGroups(items))
296+
}
297+
}

crates/uv-settings/src/combine.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use uv_configuration::{
88
};
99
use uv_distribution_types::{Index, IndexUrl, PipExtraIndex, PipFindLinks, PipIndex};
1010
use uv_install_wheel::linker::LinkMode;
11-
use uv_pypi_types::{ConflictingGroupList, SupportedEnvironments};
11+
use uv_pypi_types::{SchemaConflictingGroupList, SupportedEnvironments};
1212
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
1313
use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode};
1414

@@ -90,7 +90,7 @@ impl_combine_or!(PythonVersion);
9090
impl_combine_or!(ResolutionMode);
9191
impl_combine_or!(String);
9292
impl_combine_or!(SupportedEnvironments);
93-
impl_combine_or!(ConflictingGroupList);
93+
impl_combine_or!(SchemaConflictingGroupList);
9494
impl_combine_or!(TargetTriple);
9595
impl_combine_or!(TrustedPublishing);
9696
impl_combine_or!(Url);

crates/uv-settings/src/settings.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use uv_install_wheel::linker::LinkMode;
1111
use uv_macros::{CombineOptions, OptionsMetadata};
1212
use uv_normalize::{ExtraName, PackageName};
1313
use uv_pep508::Requirement;
14-
use uv_pypi_types::{ConflictingGroupList, SupportedEnvironments, VerbatimParsedUrl};
14+
use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
1515
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
1616
use uv_resolver::{AnnotationStyle, ExcludeNewer, PrereleaseMode, ResolutionMode};
1717

@@ -97,12 +97,12 @@ pub struct Options {
9797
#[cfg_attr(feature = "schemars", schemars(skip))]
9898
pub environments: Option<SupportedEnvironments>,
9999

100-
#[cfg_attr(feature = "schemars", schemars(skip))]
101-
pub conflicting_groups: Option<ConflictingGroupList>,
102-
103100
// NOTE(charlie): These fields should be kept in-sync with `ToolUv` in
104101
// `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct.
105102
// They're only respected in `pyproject.toml` files, and should be rejected in `uv.toml` files.
103+
#[cfg_attr(feature = "schemars", schemars(skip))]
104+
pub conflicting_groups: Option<serde::de::IgnoredAny>,
105+
106106
#[cfg_attr(feature = "schemars", schemars(skip))]
107107
pub workspace: Option<serde::de::IgnoredAny>,
108108

@@ -1558,11 +1558,11 @@ pub struct OptionsWire {
15581558
override_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
15591559
constraint_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
15601560
environments: Option<SupportedEnvironments>,
1561-
conflicting_groups: Option<ConflictingGroupList>,
15621561

15631562
// NOTE(charlie): These fields should be kept in-sync with `ToolUv` in
15641563
// `crates/uv-workspace/src/pyproject.rs`. The documentation lives on that struct.
15651564
// They're only respected in `pyproject.toml` files, and should be rejected in `uv.toml` files.
1565+
conflicting_groups: Option<serde::de::IgnoredAny>,
15661566
workspace: Option<serde::de::IgnoredAny>,
15671567
sources: Option<serde::de::IgnoredAny>,
15681568
managed: Option<serde::de::IgnoredAny>,

crates/uv-workspace/src/pyproject.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
2525
use uv_pep440::{Version, VersionSpecifiers};
2626
use uv_pep508::MarkerTree;
2727
use uv_pypi_types::{
28-
ConflictingGroupList, RequirementSource, SupportedEnvironments, VerbatimParsedUrl,
28+
ConflictingGroupList, RequirementSource, SchemaConflictingGroupList, SupportedEnvironments,
29+
VerbatimParsedUrl,
2930
};
3031

3132
#[derive(Error, Debug)]
@@ -100,6 +101,24 @@ impl PyProjectToml {
100101
false
101102
}
102103
}
104+
105+
/// Returns the set of conflicts for the project.
106+
pub fn conflicting_groups(&self) -> ConflictingGroupList {
107+
let empty = ConflictingGroupList::empty();
108+
let Some(project) = self.project.as_ref() else {
109+
return empty;
110+
};
111+
let Some(tool) = self.tool.as_ref() else {
112+
return empty;
113+
};
114+
let Some(tooluv) = tool.uv.as_ref() else {
115+
return empty;
116+
};
117+
let Some(conflicting) = tooluv.conflicting_groups.as_ref() else {
118+
return empty;
119+
};
120+
conflicting.to_conflicting_with_package_name(&project.name)
121+
}
103122
}
104123

105124
// Ignore raw document in comparison.
@@ -480,7 +499,7 @@ pub struct ToolUv {
480499
]
481500
"#
482501
)]
483-
pub conflicting_groups: Option<ConflictingGroupList>,
502+
pub conflicting_groups: Option<SchemaConflictingGroupList>,
484503
}
485504

486505
#[derive(Default, Debug, Clone, PartialEq, Eq)]

crates/uv-workspace/src/workspace.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -392,14 +392,13 @@ impl Workspace {
392392
.and_then(|uv| uv.environments.as_ref())
393393
}
394394

395-
/// Returns the set of supported environments for the workspace.
395+
/// Returns the set of conflicts for the workspace.
396396
pub fn conflicting_groups(&self) -> ConflictingGroupList {
397-
self.pyproject_toml
398-
.tool
399-
.as_ref()
400-
.and_then(|tool| tool.uv.as_ref())
401-
.and_then(|uv| uv.conflicting_groups.clone())
402-
.unwrap_or_else(ConflictingGroupList::empty)
397+
let mut conflicting = ConflictingGroupList::empty();
398+
for member in self.packages.values() {
399+
conflicting.append(&mut member.pyproject_toml.conflicting_groups());
400+
}
401+
conflicting
403402
}
404403

405404
/// Returns the set of constraints for the workspace.

crates/uv/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use uv_cli::{PythonCommand, PythonNamespace, ToolCommand, ToolNamespace, TopLeve
2424
#[cfg(feature = "self-update")]
2525
use uv_cli::{SelfCommand, SelfNamespace, SelfUpdateArgs};
2626
use uv_fs::CWD;
27+
use uv_pypi_types::ConflictingGroupList;
2728
use uv_requirements::RequirementsSource;
2829
use uv_scripts::{Pep723Item, Pep723Metadata, Pep723Script};
2930
use uv_settings::{Combine, FilesystemOptions, Options};
@@ -332,7 +333,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
332333
args.constraints_from_workspace,
333334
args.overrides_from_workspace,
334335
args.environments,
335-
args.conflicting_groups,
336+
ConflictingGroupList::empty(),
336337
args.settings.extras,
337338
args.settings.output_file.as_deref(),
338339
args.settings.resolution,

crates/uv/src/settings.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl}
2929
use uv_install_wheel::linker::LinkMode;
3030
use uv_normalize::PackageName;
3131
use uv_pep508::{ExtraName, RequirementOrigin};
32-
use uv_pypi_types::{ConflictingGroupList, Requirement, SupportedEnvironments};
32+
use uv_pypi_types::{Requirement, SupportedEnvironments};
3333
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
3434
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode};
3535
use uv_settings::{
@@ -1240,7 +1240,6 @@ pub(crate) struct PipCompileSettings {
12401240
pub(crate) constraints_from_workspace: Vec<Requirement>,
12411241
pub(crate) overrides_from_workspace: Vec<Requirement>,
12421242
pub(crate) environments: SupportedEnvironments,
1243-
pub(crate) conflicting_groups: ConflictingGroupList,
12441243
pub(crate) refresh: Refresh,
12451244
pub(crate) settings: PipSettings,
12461245
}
@@ -1332,12 +1331,6 @@ impl PipCompileSettings {
13321331
SupportedEnvironments::default()
13331332
};
13341333

1335-
let conflicting_groups = if let Some(configuration) = &filesystem {
1336-
configuration.conflicting_groups.clone().unwrap_or_default()
1337-
} else {
1338-
ConflictingGroupList::empty()
1339-
};
1340-
13411334
Self {
13421335
src_file,
13431336
constraint: constraint
@@ -1355,7 +1348,6 @@ impl PipCompileSettings {
13551348
constraints_from_workspace,
13561349
overrides_from_workspace,
13571350
environments,
1358-
conflicting_groups,
13591351
refresh: Refresh::from(refresh),
13601352
settings: PipSettings::combine(
13611353
PipOptions {

0 commit comments

Comments
 (0)