diff --git a/crates/uv-pep508/src/marker/tree.rs b/crates/uv-pep508/src/marker/tree.rs index 450096e50..d493fe4b1 100644 --- a/crates/uv-pep508/src/marker/tree.rs +++ b/crates/uv-pep508/src/marker/tree.rs @@ -1158,6 +1158,32 @@ impl MarkerTree { } } + /// Returns true if this marker simplifies to true if the given set of extras is activated. + pub fn evaluate_only_extras(self, extras: &[ExtraName]) -> bool { + match self.kind() { + MarkerTreeKind::True => true, + MarkerTreeKind::False => false, + MarkerTreeKind::Version(marker) => marker + .edges() + .all(|(_, tree)| tree.evaluate_only_extras(extras)), + MarkerTreeKind::String(marker) => marker + .children() + .all(|(_, tree)| tree.evaluate_only_extras(extras)), + MarkerTreeKind::In(marker) => marker + .children() + .all(|(_, tree)| tree.evaluate_only_extras(extras)), + MarkerTreeKind::Contains(marker) => marker + .children() + .all(|(_, tree)| tree.evaluate_only_extras(extras)), + MarkerTreeKind::List(marker) => marker + .children() + .all(|(_, tree)| tree.evaluate_only_extras(extras)), + MarkerTreeKind::Extra(marker) => marker + .edge(extras.contains(marker.name().extra())) + .evaluate_only_extras(extras), + } + } + /// Find a top level `extra == "..."` expression. /// /// ASSUMPTION: There is one `extra = "..."`, and it's either the only marker or part of the @@ -3660,4 +3686,27 @@ mod test { let right = "python_full_version == '3.10.*'"; assert_eq!(left, right, "{left} != {right}"); } + + #[test] + fn evaluate_only_extras() { + let a = ExtraName::from_str("a").unwrap(); + let b = ExtraName::from_str("b").unwrap(); + + let marker = m("extra == 'a' and extra == 'b'"); + assert!(!marker.evaluate_only_extras(&[a.clone()])); + assert!(!marker.evaluate_only_extras(&[b.clone()])); + assert!(marker.evaluate_only_extras(&[a.clone(), b.clone()])); + + let marker = m("(platform_machine == 'inapplicable' and extra == 'b') or extra == 'a'"); + assert!(marker.evaluate_only_extras(&[a.clone()])); + assert!(!marker.evaluate_only_extras(&[b.clone()])); + assert!(marker.evaluate_only_extras(&[a.clone(), b.clone()])); + + let marker = m( + "(platform_machine == 'inapplicable' and extra == 'a') or (platform_machine != 'inapplicable' and extra == 'b')", + ); + assert!(!marker.evaluate_only_extras(&[a.clone()])); + assert!(!marker.evaluate_only_extras(&[b.clone()])); + assert!(marker.evaluate_only_extras(&[a.clone(), b.clone()])); + } } diff --git a/crates/uv-pypi-types/src/conflicts.rs b/crates/uv-pypi-types/src/conflicts.rs index 5ebeb160a..0dcd1393d 100644 --- a/crates/uv-pypi-types/src/conflicts.rs +++ b/crates/uv-pypi-types/src/conflicts.rs @@ -756,3 +756,24 @@ impl From for ConflictItemWire { } } } + +/// An inference about whether a conflicting item is always included or +/// excluded. +/// +/// We collect these for each node in the graph after determining which +/// extras/groups are activated for each node. Once we know what's +/// activated, we can infer what must also be *inactivated* based on what's +/// conflicting with it. So for example, if we have a conflict marker like +/// `extra == 'foo' and extra != 'bar'`, and `foo` and `bar` have been +/// declared as conflicting, and we are in a part of the graph where we +/// know `foo` must be activated, then it follows that `extra != 'bar'` +/// must always be true. Because if it were false, it would imply both +/// `foo` and `bar` were activated simultaneously, which uv guarantees +/// won't happen. +/// +/// We then use these inferences to simplify the conflict markers. +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct Inference { + pub included: bool, + pub item: ConflictItem, +} diff --git a/crates/uv-resolver/src/graph_ops.rs b/crates/uv-resolver/src/graph_ops.rs index 878074e77..84143326b 100644 --- a/crates/uv-resolver/src/graph_ops.rs +++ b/crates/uv-resolver/src/graph_ops.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::collections::hash_map::Entry; use petgraph::graph::{EdgeIndex, NodeIndex}; @@ -6,7 +7,7 @@ use petgraph::{Direction, Graph}; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use uv_pep508::MarkerTree; -use uv_pypi_types::{ConflictItem, Conflicts}; +use uv_pypi_types::{ConflictItem, Conflicts, Inference}; use crate::resolution::ResolutionGraphNode; use crate::universal_marker::UniversalMarker; @@ -99,27 +100,6 @@ pub(crate) fn simplify_conflict_markers( conflicts: &Conflicts, graph: &mut Graph, ) { - /// An inference about whether a conflicting item is always included or - /// excluded. - /// - /// We collect these for each node in the graph after determining which - /// extras/groups are activated for each node. Once we know what's - /// activated, we can infer what must also be *inactivated* based on what's - /// conflicting with it. So for example, if we have a conflict marker like - /// `extra == 'foo' and extra != 'bar'`, and `foo` and `bar` have been - /// declared as conflicting, and we are in a part of the graph where we - /// know `foo` must be activated, then it follows that `extra != 'bar'` - /// must always be true. Because if it were false, it would imply both - /// `foo` and `bar` were activated simultaneously, which uv guarantees - /// won't happen. - /// - /// We then use these inferences to simplify the conflict markers. - #[derive(Clone, Debug, Eq, Hash, PartialEq)] - struct Inference { - item: ConflictItem, - included: bool, - } - // Do nothing if there are no declared conflicts. Without any declared // conflicts, we know we have no conflict markers and thus nothing to // simplify by determining which extras are activated at different points @@ -198,11 +178,11 @@ pub(crate) fn simplify_conflict_markers( } } - let mut inferences: FxHashMap>> = FxHashMap::default(); + let mut inferences: FxHashMap>> = FxHashMap::default(); for (node_id, sets) in activated { let mut new_sets = Vec::with_capacity(sets.len()); for set in sets { - let mut new_set = FxHashSet::default(); + let mut new_set = BTreeSet::default(); for item in set { for conflict_set in conflicts.iter() { if !conflict_set.contains(item.package(), item.as_ref().conflict()) { @@ -270,19 +250,27 @@ pub(crate) fn simplify_conflict_markers( Some((inf.item.package(), inf.item.group()?)) }) .collect::>(); - graph[edge_index].conflict().evaluate(&extras, &groups) + // Notably, the marker must be possible to satisfy with the extras and groups alone. + // For example, when `a` and `b` conflict, this marker does not simplify: + // ``` + // (platform_machine == 'x86_64' and extra == 'extra-5-foo-b') or extra == 'extra-5-foo-a' + // ```` + graph[edge_index].evaluate_only_extras(&extras, &groups) }); - if !all_paths_satisfied { - continue; - } - for set in inference_sets { - for inf in set { - if inf.included { - graph[edge_index].assume_conflict_item(&inf.item); - } else { - graph[edge_index].assume_not_conflict_item(&inf.item); + if all_paths_satisfied { + for set in inference_sets { + for inf in set { + // TODO(konsti): Now that `Inference` is public, move more `included` handling + // to `UniversalMarker`. + if inf.included { + graph[edge_index].assume_conflict_item(&inf.item); + } else { + graph[edge_index].assume_not_conflict_item(&inf.item); + } } } + } else { + graph[edge_index].unify_inference_sets(inference_sets); } } } diff --git a/crates/uv-resolver/src/resolver/environment.rs b/crates/uv-resolver/src/resolver/environment.rs index 8faec48c4..cab258aea 100644 --- a/crates/uv-resolver/src/resolver/environment.rs +++ b/crates/uv-resolver/src/resolver/environment.rs @@ -1,9 +1,13 @@ +use std::collections::BTreeSet; use std::sync::Arc; + +use itertools::Itertools; use tracing::trace; + use uv_distribution_types::{RequiresPython, RequiresPythonRange}; use uv_pep440::VersionSpecifiers; use uv_pep508::{MarkerEnvironment, MarkerTree}; -use uv_pypi_types::{ConflictItem, ConflictItemRef, ResolverMarkerEnvironment}; +use uv_pypi_types::{ConflictItem, ConflictItemRef, ConflictPackage, ResolverMarkerEnvironment}; use crate::pubgrub::{PubGrubDependency, PubGrubPackage}; use crate::resolver::ForkState; @@ -374,15 +378,62 @@ impl ResolverEnvironment { /// This is useful in contexts where one wants to display a message /// relating to a particular fork, but either no message or an entirely /// different message when this isn't a fork. - pub(crate) fn end_user_fork_display(&self) -> Option { - match self.kind { + pub(crate) fn end_user_fork_display(&self) -> Option { + match &self.kind { Kind::Specific { .. } => None, - Kind::Universal { ref markers, .. } => { - if markers.is_true() { - None - } else { - Some(format!("split ({markers:?})")) + Kind::Universal { + initial_forks: _, + markers, + include, + exclude, + } => { + let format_conflict_item = |conflict_item: &ConflictItem| { + format!( + "{}{}", + conflict_item.package(), + match conflict_item.conflict() { + ConflictPackage::Extra(extra) => format!("[{extra}]"), + ConflictPackage::Group(group) => { + format!("[group:{group}]") + } + } + ) + }; + + if markers.is_true() && include.is_empty() && exclude.is_empty() { + return None; } + + let mut descriptors = Vec::new(); + if !markers.is_true() { + descriptors.push(format!("markers: {markers:?}")); + } + if !include.is_empty() { + descriptors.push(format!( + "included: {}", + // Sort to ensure stable error messages + include + .iter() + .map(format_conflict_item) + .collect::>() + .into_iter() + .join(", "), + )); + } + if !exclude.is_empty() { + descriptors.push(format!( + "excluded: {}", + // Sort to ensure stable error messages + exclude + .iter() + .map(format_conflict_item) + .collect::>() + .into_iter() + .join(", "), + )); + } + + Some(format!("split ({})", descriptors.join("; "))) } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 4d0ecf125..eed7a3955 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -708,12 +708,19 @@ impl ResolverState = resolution + .nodes + .keys() + .map(|package| &package.name) + .collect(); + debug!( + "Distinct solution for {env} with {} package(s)", + packages.len() + ); + } } } for resolution in &resolutions { diff --git a/crates/uv-resolver/src/universal_marker.rs b/crates/uv-resolver/src/universal_marker.rs index e0643520b..c40aaf77a 100644 --- a/crates/uv-resolver/src/universal_marker.rs +++ b/crates/uv-resolver/src/universal_marker.rs @@ -1,15 +1,13 @@ use std::borrow::Borrow; +use std::collections::BTreeSet; use std::str::FromStr; use itertools::Itertools; use rustc_hash::FxHashMap; use uv_normalize::{ExtraName, GroupName, PackageName}; -use uv_pep508::{ - ExtraOperator, MarkerEnvironment, MarkerEnvironmentBuilder, MarkerExpression, MarkerOperator, - MarkerTree, -}; -use uv_pypi_types::{ConflictItem, ConflictPackage, Conflicts}; +use uv_pep508::{ExtraOperator, MarkerEnvironment, MarkerExpression, MarkerOperator, MarkerTree}; +use uv_pypi_types::{ConflictItem, ConflictPackage, Conflicts, Inference}; use crate::ResolveError; @@ -140,6 +138,36 @@ impl UniversalMarker { self.pep508 = self.marker.without_extras(); } + /// If all inference sets reduce to the same marker, simplify the marker using that knowledge. + pub(crate) fn unify_inference_sets(&mut self, conflict_sets: &[BTreeSet]) { + let mut previous_marker = None; + + for conflict_set in conflict_sets { + let mut marker = self.marker; + for inference in conflict_set { + let extra = encode_conflict_item(&inference.item); + + marker = if inference.included { + marker.simplify_extras_with(|candidate| *candidate == extra) + } else { + marker.simplify_not_extras_with(|candidate| *candidate == extra) + }; + } + if let Some(previous_marker) = &previous_marker { + if previous_marker != &marker { + return; + } + } else { + previous_marker = Some(marker); + } + } + + if let Some(all_branches_marker) = previous_marker { + self.marker = all_branches_marker; + self.pep508 = self.marker.without_extras(); + } + } + /// Assumes that a given extra/group for the given package is activated. /// /// This may simplify the conflicting marker component of this universal @@ -265,6 +293,23 @@ impl UniversalMarker { .evaluate(env, &extras.chain(groups).collect::>()) } + /// Returns true if the marker always evaluates to true if the given set of extras is activated. + pub(crate) fn evaluate_only_extras(self, extras: &[(P, E)], groups: &[(P, G)]) -> bool + where + P: Borrow, + E: Borrow, + G: Borrow, + { + let extras = extras + .iter() + .map(|(package, extra)| encode_package_extra(package.borrow(), extra.borrow())); + let groups = groups + .iter() + .map(|(package, group)| encode_package_group(package.borrow(), group.borrow())); + self.marker + .evaluate_only_extras(&extras.chain(groups).collect::>()) + } + /// Returns the internal marker that combines both the PEP 508 /// and conflict marker. pub fn combined(self) -> MarkerTree { @@ -421,40 +466,6 @@ impl ConflictMarker { self.marker.is_false() } - /// Returns true if this conflict marker is satisfied by the given - /// list of activated extras and groups. - pub(crate) fn evaluate(self, extras: &[(P, E)], groups: &[(P, G)]) -> bool - where - P: Borrow, - E: Borrow, - G: Borrow, - { - static DUMMY: std::sync::LazyLock = std::sync::LazyLock::new(|| { - MarkerEnvironment::try_from(MarkerEnvironmentBuilder { - implementation_name: "", - implementation_version: "3.7", - os_name: "linux", - platform_machine: "", - platform_python_implementation: "", - platform_release: "", - platform_system: "", - platform_version: "", - python_full_version: "3.7", - python_version: "3.7", - sys_platform: "linux", - }) - .unwrap() - }); - let extras = extras - .iter() - .map(|(package, extra)| encode_package_extra(package.borrow(), extra.borrow())); - let groups = groups - .iter() - .map(|(package, group)| encode_package_group(package.borrow(), group.borrow())); - self.marker - .evaluate(&DUMMY, &extras.chain(groups).collect::>()) - } - /// Returns inclusion and exclusion (respectively) conflict items parsed /// from this conflict marker. /// @@ -491,6 +502,14 @@ impl std::fmt::Debug for ConflictMarker { } } +/// Encodes the given conflict into a valid `extra` value in a PEP 508 marker. +fn encode_conflict_item(conflict: &ConflictItem) -> ExtraName { + match conflict.conflict() { + ConflictPackage::Extra(extra) => encode_package_extra(conflict.package(), extra), + ConflictPackage::Group(group) => encode_package_group(conflict.package(), group), + } +} + /// Encodes the given package name and its corresponding extra into a valid /// `extra` value in a PEP 508 marker. fn encode_package_extra(package: &PackageName, extra: &ExtraName) -> ExtraName { @@ -791,7 +810,7 @@ mod tests { /// This is just the underlying marker. And if it's `true`, then a /// non-conforming `true` string is returned. (Which is fine since /// this is just for tests.) - fn tostr(cm: ConflictMarker) -> String { + fn to_str(cm: ConflictMarker) -> String { cm.marker .try_to_string() .unwrap_or_else(|| "true".to_string()) @@ -805,14 +824,14 @@ mod tests { let conflicts = create_conflicts([create_set(["foo", "bar"])]); let cm = ConflictMarker::from_conflicts(&conflicts); assert_eq!( - tostr(cm), + to_str(cm), "extra != 'extra-3-pkg-foo' or extra != 'extra-3-pkg-bar'" ); let conflicts = create_conflicts([create_set(["foo", "bar", "baz"])]); let cm = ConflictMarker::from_conflicts(&conflicts); assert_eq!( - tostr(cm), + to_str(cm), "(extra != 'extra-3-pkg-baz' and extra != 'extra-3-pkg-foo') \ or (extra != 'extra-3-pkg-bar' and extra != 'extra-3-pkg-foo') \ or (extra != 'extra-3-pkg-bar' and extra != 'extra-3-pkg-baz')", @@ -821,7 +840,7 @@ mod tests { let conflicts = create_conflicts([create_set(["foo", "bar"]), create_set(["fox", "ant"])]); let cm = ConflictMarker::from_conflicts(&conflicts); assert_eq!( - tostr(cm), + to_str(cm), "(extra != 'extra-3-pkg-bar' and extra != 'extra-3-pkg-fox') or \ (extra != 'extra-3-pkg-ant' and extra != 'extra-3-pkg-foo') or \ (extra != 'extra-3-pkg-ant' and extra != 'extra-3-pkg-bar') or \ @@ -856,7 +875,7 @@ mod tests { .collect::>(); let groups = Vec::<(PackageName, GroupName)>::new(); assert!( - !cm.evaluate(&extras, &groups), + !UniversalMarker::new(MarkerTree::TRUE, cm).evaluate_only_extras(&extras, &groups), "expected `{extra_names:?}` to evaluate to `false` in `{cm:?}`" ); } @@ -879,7 +898,7 @@ mod tests { .collect::>(); let groups = Vec::<(PackageName, GroupName)>::new(); assert!( - cm.evaluate(&extras, &groups), + UniversalMarker::new(MarkerTree::TRUE, cm).evaluate_only_extras(&extras, &groups), "expected `{extra_names:?}` to evaluate to `true` in `{cm:?}`" ); } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index ef8075e3d..88322d794 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -3671,7 +3671,7 @@ fn lock_requires_python() -> Result<()> { ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies for split (python_full_version >= '3.7' and python_full_version < '3.7.9'): + × No solution found when resolving dependencies for split (markers: python_full_version >= '3.7' and python_full_version < '3.7.9'): ╰─▶ Because the requested Python version (>=3.7) does not satisfy Python>=3.7.9 and pygls>=1.1.0,<=1.2.1 depends on Python>=3.7.9,<4, we can conclude that pygls>=1.1.0,<=1.2.1 cannot be used. And because only the following versions of pygls are available: pygls<=1.1.0 @@ -28520,7 +28520,7 @@ fn lock_conflict_for_disjoint_python_version() -> Result<()> { ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies for split (python_full_version >= '3.11'): + × No solution found when resolving dependencies for split (markers: python_full_version >= '3.11'): ╰─▶ Because only the following versions of numpy{python_full_version >= '3.10'} are available: numpy{python_full_version >= '3.10'}<=1.21.0 numpy{python_full_version >= '3.10'}==1.21.1 @@ -28774,7 +28774,7 @@ fn lock_conflict_for_disjoint_platform() -> Result<()> { ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies for split (sys_platform == 'exotic'): + × No solution found when resolving dependencies for split (markers: sys_platform == 'exotic'): ╰─▶ Because only the following versions of numpy{sys_platform == 'exotic'} are available: numpy{sys_platform == 'exotic'}<=1.24.0 numpy{sys_platform == 'exotic'}==1.24.1 diff --git a/crates/uv/tests/it/lock_conflict.rs b/crates/uv/tests/it/lock_conflict.rs index b809d2199..5305289c4 100644 --- a/crates/uv/tests/it/lock_conflict.rs +++ b/crates/uv/tests/it/lock_conflict.rs @@ -546,16 +546,16 @@ fn extra_multiple_not_conflicting2() -> Result<()> { project4 = ["sortedcontainers==2.4.0"] "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: + × No solution found when resolving dependencies for split (included: project[extra2], project[project3]; excluded: project[extra1], project[project4]): ╰─▶ Because project[project3] depends on sortedcontainers==2.3.0 and project[extra2] depends on sortedcontainers==2.4.0, we can conclude that project[extra2] and project[project3] are incompatible. And because your project requires project[extra2] and project[project3], we can conclude that your project's requirements are unsatisfiable. - "###); + "); // One could try to declare all pairs of conflicting extras as // conflicting, but this doesn't quite work either. For example, @@ -703,16 +703,16 @@ fn extra_multiple_independent() -> Result<()> { project4 = ["anyio==4.2.0"] "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: + × No solution found when resolving dependencies for split (included: project[project4]; excluded: project[project3]): ╰─▶ Because project[extra2] depends on sortedcontainers==2.4.0 and project[extra1] depends on sortedcontainers==2.3.0, we can conclude that project[extra1] and project[extra2] are incompatible. And because your project requires project[extra1] and project[extra2], we can conclude that your project's requirements are unsatisfiable. - "###); + "); // Once we declare ALL our conflicting extras, resolution succeeds. let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -1551,20 +1551,20 @@ fn extra_nested_across_workspace() -> Result<()> { // `dummy[extra1]` conflicts with `dummysub[extra2]` and that // `dummy[extra2]` conflicts with `dummysub[extra1]`. So we end // up with a resolution failure. - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock(), @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: + × No solution found when resolving dependencies for split (included: dummy[extra2], dummysub[extra1]; excluded: dummy[extra1], dummysub[extra2]): ╰─▶ Because dummy[extra2] depends on proxy1[extra2] and only proxy1[extra2]==0.1.0 is available, we can conclude that dummy[extra2] depends on proxy1[extra2]==0.1.0. (1) Because proxy1[extra1]==0.1.0 depends on anyio==4.1.0 and proxy1[extra2]==0.1.0 depends on anyio==4.2.0, we can conclude that proxy1[extra1]==0.1.0 and proxy1[extra2]==0.1.0 are incompatible. And because we know from (1) that dummy[extra2] depends on proxy1[extra2]==0.1.0, we can conclude that dummy[extra2] and proxy1[extra1]==0.1.0 are incompatible. And because only proxy1[extra1]==0.1.0 is available and dummysub[extra1] depends on proxy1[extra1], we can conclude that dummysub[extra1] and dummy[extra2] are incompatible. And because your workspace requires dummy[extra2] and dummysub[extra1], we can conclude that your workspace's requirements are unsatisfiable. - "###); + "); // Now let's write out the full set of conflicts, taking // advantage of the optional `package` key. @@ -1696,7 +1696,7 @@ fn extra_depends_on_conflicting_extra() -> Result<()> { ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: + × No solution found when resolving dependencies for split (included: example[foo]; excluded: example[bar]): ╰─▶ Because example[foo] depends on sortedcontainers==2.3.0 and sortedcontainers==2.4.0, we can conclude that example[foo]'s requirements are unsatisfiable. And because your project requires example[foo], we can conclude that your project's requirements are unsatisfiable. "); @@ -6005,7 +6005,7 @@ fn extra_inferences() -> Result<()> { version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-3-pkg-x1' and extra == 'extra-3-pkg-x2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" } wheels = [ @@ -6026,7 +6026,7 @@ fn extra_inferences() -> Result<()> { version = "4.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-3-pkg-x1' and extra == 'extra-3-pkg-x2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/32/cdfba08674d72fe7895a8ec7be8f171e8502274999cae9497e4545404873/colorlog-4.8.0.tar.gz", hash = "sha256:59b53160c60902c405cdec28d38356e09d40686659048893e026ecbd589516b1", size = 28770, upload-time = "2021-03-22T11:26:32.319Z" } wheels = [ @@ -6100,7 +6100,7 @@ fn extra_inferences() -> Result<()> { version = "42.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'extra-3-pkg-x1' and extra == 'extra-3-pkg-x2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/9e/a55763a32d340d7b06d045753c186b690e7d88780cafce5f88cb931536be/cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", size = 671025, upload-time = "2024-02-24T01:17:48.141Z" } wheels = [ @@ -6804,7 +6804,7 @@ fn extra_inferences() -> Result<()> { source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, - { name = "time-machine", marker = "implementation_name != 'pypy'" }, + { name = "time-machine", marker = "implementation_name != 'pypy' or (extra == 'extra-3-pkg-x1' and extra == 'extra-3-pkg-x2')" }, { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/fe/27c7438c6ac8b8f8bef3c6e571855602ee784b85d072efddfff0ceb1cd77/pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e", size = 84524, upload-time = "2023-12-16T21:27:19.742Z" } @@ -7189,7 +7189,7 @@ fn extra_inferences() -> Result<()> { version = "1.4.52" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' or (extra == 'extra-3-pkg-x1' and extra == 'extra-3-pkg-x2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/a4/b5991829c34af0505e0f2b1ccf9588d1ba90f2d984ee208c90c985f1265a/SQLAlchemy-1.4.52.tar.gz", hash = "sha256:80e63bbdc5217dad3485059bdf6f65a7d43f33c8bde619df5c220edf03d87296", size = 8514200, upload-time = "2024-03-04T13:29:44.258Z" } wheels = [ @@ -10165,7 +10165,7 @@ fn incorrect_extra_simplification_leads_to_multiple_torch_packages() -> Result<( version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-4-test-chgnet' and extra == 'extra-4-test-m3gnet')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ @@ -10511,7 +10511,7 @@ fn duplicate_torch_and_sympy_because_of_wrong_inferences() -> Result<()> { dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'extra-4-test-alignn' and extra == 'extra-4-test-all') or (extra == 'extra-4-test-alignn' and extra == 'extra-4-test-chgnet') or (extra == 'extra-4-test-all' and extra == 'extra-4-test-m3gnet') or (extra == 'extra-4-test-chgnet' and extra == 'extra-4-test-m3gnet')" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, @@ -11621,7 +11621,7 @@ fn duplicate_torch_and_sympy_because_of_wrong_inferences() -> Result<()> { version = "6.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'extra-4-test-alignn' and extra == 'extra-4-test-all') or (extra == 'extra-4-test-alignn' and extra == 'extra-4-test-chgnet') or (extra == 'extra-4-test-all' and extra == 'extra-4-test-m3gnet') or (extra == 'extra-4-test-chgnet' and extra == 'extra-4-test-m3gnet')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002, upload-time = "2024-09-09T23:49:38.163Z" } wheels = [ @@ -13398,7 +13398,7 @@ fn duplicate_torch_and_sympy_because_of_wrong_inferences() -> Result<()> { version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'extra-4-test-alignn' and extra == 'extra-4-test-all') or (extra == 'extra-4-test-alignn' and extra == 'extra-4-test-chgnet') or (extra == 'extra-4-test-all' and extra == 'extra-4-test-m3gnet') or (extra == 'extra-4-test-chgnet' and extra == 'extra-4-test-m3gnet')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ @@ -14159,10 +14159,10 @@ fn overlapping_resolution_markers() -> Result<()> { "sys_platform == 'darwin' and extra == 'extra-14-ads-mega-model-cpu' and extra != 'extra-14-ads-mega-model-cu118'", ] dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx" }, + { name = "filelock", marker = "(sys_platform != 'darwin' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform == 'darwin' and extra == 'extra-14-ads-mega-model-cpu') or (extra != 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, + { name = "fsspec", marker = "(sys_platform != 'darwin' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform == 'darwin' and extra == 'extra-14-ads-mega-model-cpu') or (extra != 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, + { name = "jinja2", marker = "(sys_platform != 'darwin' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform == 'darwin' and extra == 'extra-14-ads-mega-model-cpu') or (extra != 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, + { name = "networkx", marker = "(sys_platform != 'darwin' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform == 'darwin' and extra == 'extra-14-ads-mega-model-cpu') or (extra != 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-14-ads-mega-model-cu118') or (platform_machine != 'x86_64' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform != 'linux' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, { name = "nvidia-cuda-cupti-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-14-ads-mega-model-cu118') or (platform_machine != 'x86_64' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform != 'linux' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-14-ads-mega-model-cu118') or (platform_machine != 'x86_64' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform != 'linux' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, @@ -14174,9 +14174,9 @@ fn overlapping_resolution_markers() -> Result<()> { { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-14-ads-mega-model-cu118') or (platform_machine != 'x86_64' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform != 'linux' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, { name = "nvidia-nccl-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-14-ads-mega-model-cu118') or (platform_machine != 'x86_64' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform != 'linux' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, { name = "nvidia-nvtx-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-14-ads-mega-model-cu118') or (platform_machine != 'x86_64' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform != 'linux' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, - { name = "sympy" }, + { name = "sympy", marker = "(sys_platform != 'darwin' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform == 'darwin' and extra == 'extra-14-ads-mega-model-cpu') or (extra != 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, { name = "triton", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-14-ads-mega-model-cu118') or (platform_machine != 'x86_64' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform != 'linux' and extra == 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "(sys_platform != 'darwin' and extra == 'extra-14-ads-mega-model-cu118') or (sys_platform == 'darwin' and extra == 'extra-14-ads-mega-model-cpu') or (extra != 'extra-14-ads-mega-model-cpu' and extra == 'extra-14-ads-mega-model-cu118')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/33/b3/1fcc3bccfddadfd6845dcbfe26eb4b099f1dfea5aa0e5cfb92b3c98dba5b/torch-2.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bc889d311a855dd2dfd164daf8cc903a6b7273a747189cebafdd89106e4ad585", size = 755526581, upload-time = "2024-03-27T21:06:46.5Z" }, @@ -15143,3 +15143,160 @@ fn avoids_exponential_lock_file_growth() -> Result<()> { Ok(()) } + +/// Check that simplification of conflict markers does not apply with not all paths activate the +/// marker unconditionally. +/// +/// For example, when `a` and `b` conflict, this marker does not simplify: +/// ```text +/// (platform_machine == 'x86_64' and extra == 'extra-5-foo-b') or extra == 'extra-5-foo-a' +/// ```` +/// +/// Ref: +#[test] +fn do_not_simplify_if_not_all_conflict_extras_satisfy_the_marker_by_themselves() -> Result<()> { + let context = TestContext::new("3.12").with_exclude_newer("2025-02-06T00:00Z"); + + let pyproject = r#" + [project] + name = "debug" + version = "0.0.1" + requires-python = "==3.12.*" + + [project.optional-dependencies] + a = [ + "python-dateutil==2.8.0", + ] + b = [ + "python-dateutil==2.8.1; platform_machine != 'inapplicable'", + "python-dateutil==2.8.0; platform_machine == 'inapplicable'", + ] + + [tool.uv] + conflicts = [ + [ + { extra = "a" }, + { extra = "b" }, + ], + ] + "#; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(pyproject)?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "); + + let lock = context.read("uv.lock"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, + @r#" + version = 1 + revision = 3 + requires-python = "==3.12.*" + resolution-markers = [ + "platform_machine != 'inapplicable' and extra != 'extra-5-debug-a' and extra == 'extra-5-debug-b'", + "platform_machine == 'inapplicable' and extra != 'extra-5-debug-a' and extra == 'extra-5-debug-b'", + "extra == 'extra-5-debug-a' and extra != 'extra-5-debug-b'", + "extra != 'extra-5-debug-a' and extra != 'extra-5-debug-b'", + ] + conflicts = [[ + { package = "debug", extra = "a" }, + { package = "debug", extra = "b" }, + ]] + + [options] + exclude-newer = "2025-02-06T00:00:00Z" + + [[package]] + name = "debug" + version = "0.0.1" + source = { virtual = "." } + + [package.optional-dependencies] + a = [ + { name = "python-dateutil", version = "2.8.0", source = { registry = "https://pypi.org/simple" } }, + ] + b = [ + { name = "python-dateutil", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'inapplicable' and extra == 'extra-5-debug-b') or (extra == 'extra-5-debug-a' and extra == 'extra-5-debug-b')" }, + { name = "python-dateutil", version = "2.8.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'inapplicable' and extra == 'extra-5-debug-b') or (extra == 'extra-5-debug-a' and extra == 'extra-5-debug-b')" }, + ] + + [package.metadata] + requires-dist = [ + { name = "python-dateutil", marker = "platform_machine == 'inapplicable' and extra == 'b'", specifier = "==2.8.0" }, + { name = "python-dateutil", marker = "platform_machine != 'inapplicable' and extra == 'b'", specifier = "==2.8.1" }, + { name = "python-dateutil", marker = "extra == 'a'", specifier = "==2.8.0" }, + ] + provides-extras = ["a", "b"] + + [[package]] + name = "python-dateutil" + version = "2.8.0" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "platform_machine == 'inapplicable' and extra != 'extra-5-debug-a' and extra == 'extra-5-debug-b'", + "extra == 'extra-5-debug-a' and extra != 'extra-5-debug-b'", + ] + dependencies = [ + { name = "six", marker = "(platform_machine == 'inapplicable' and extra == 'extra-5-debug-b') or extra == 'extra-5-debug-a'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/ad/99/5b2e99737edeb28c71bcbec5b5dda19d0d9ef3ca3e92e3e925e7c0bb364c/python-dateutil-2.8.0.tar.gz", hash = "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e", size = 327134, upload-time = "2019-02-05T14:12:37.493Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/41/17/c62faccbfbd163c7f57f3844689e3a78bae1f403648a6afb1d0866d87fbb/python_dateutil-2.8.0-py2.py3-none-any.whl", hash = "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", size = 226803, upload-time = "2019-02-05T14:12:35.322Z" }, + ] + + [[package]] + name = "python-dateutil" + version = "2.8.1" + source = { registry = "https://pypi.org/simple" } + resolution-markers = [ + "platform_machine != 'inapplicable'", + ] + dependencies = [ + { name = "six", marker = "platform_machine != 'inapplicable'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/be/ed/5bbc91f03fa4c839c4c7360375da77f9659af5f7086b7a7bdda65771c8e0/python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", size = 331745, upload-time = "2019-11-03T05:42:03.923Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/70/d60450c3dd48ef87586924207ae8907090de0b306af2bce5d134d78615cb/python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a", size = 227183, upload-time = "2019-11-03T05:42:01.643Z" }, + ] + + [[package]] + name = "six" + version = "1.17.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, + ] + "# + ); + }); + + // The incorrect behavior was that only python-dateutil was installed and six was missing. + uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("a").arg("--dry-run"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Would use project environment at: .venv + Resolved 4 packages in [TIME] + Found up-to-date lockfile at: uv.lock + Would download 2 packages + Would install 2 packages + + python-dateutil==2.8.0 + + six==1.17.0 + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index f1a72361f..ac1771b63 100644 --- a/crates/uv/tests/it/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -818,7 +818,7 @@ fn conflict_in_fork() -> Result<()> { ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies for split (sys_platform == 'os2'): + × No solution found when resolving dependencies for split (markers: sys_platform == 'os2'): ╰─▶ Because only package-b==1.0.0 is available and package-b==1.0.0 depends on package-d==1, we can conclude that all versions of package-b depend on package-d==1. And because package-c==1.0.0 depends on package-d==2 and only package-c==1.0.0 is available, we can conclude that all versions of package-b and all versions of package-c are incompatible. And because package-a==1.0.0 depends on package-b and package-c, we can conclude that package-a==1.0.0 cannot be used. diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 5bba08873..2971930ca 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -14569,7 +14569,7 @@ fn unsupported_requires_python_dynamic_metadata() -> Result<()> { ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies for split (python_full_version >= '3.10'): + × No solution found when resolving dependencies for split (markers: python_full_version >= '3.10'): ╰─▶ Because source-distribution==0.0.3 requires Python >=3.10 and you require source-distribution{python_full_version >= '3.10'}==0.0.3, we can conclude that your requirements are unsatisfiable. hint: The source distribution for `source-distribution` (v0.0.3) does not include static metadata. Generating metadata for this package requires Python >=3.10, but Python 3.8.[X] is installed. diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index edda357d8..277704197 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -666,7 +666,7 @@ fn group_requires_python_useful_defaults() -> Result<()> { ----- stderr ----- Using CPython 3.8.[X] interpreter at: [PYTHON-3.8] Creating virtual environment at: .venv - × No solution found when resolving dependencies for split (python_full_version == '3.8.*'): + × No solution found when resolving dependencies for split (markers: python_full_version == '3.8.*'): ╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used. And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used. And because pharaohs-tomp:dev depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:dev, we can conclude that your project's requirements are unsatisfiable. @@ -681,7 +681,7 @@ fn group_requires_python_useful_defaults() -> Result<()> { ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies for split (python_full_version == '3.8.*'): + × No solution found when resolving dependencies for split (markers: python_full_version == '3.8.*'): ╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used. And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used. And because pharaohs-tomp:dev depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:dev, we can conclude that your project's requirements are unsatisfiable. @@ -810,7 +810,7 @@ fn group_requires_python_useful_non_defaults() -> Result<()> { ----- stderr ----- Using CPython 3.8.[X] interpreter at: [PYTHON-3.8] Creating virtual environment at: .venv - × No solution found when resolving dependencies for split (python_full_version == '3.8.*'): + × No solution found when resolving dependencies for split (markers: python_full_version == '3.8.*'): ╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used. And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used. And because pharaohs-tomp:mygroup depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:mygroup, we can conclude that your project's requirements are unsatisfiable. @@ -826,7 +826,7 @@ fn group_requires_python_useful_non_defaults() -> Result<()> { ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies for split (python_full_version == '3.8.*'): + × No solution found when resolving dependencies for split (markers: python_full_version == '3.8.*'): ╰─▶ Because the requested Python version (>=3.8) does not satisfy Python>=3.9 and sphinx==7.2.6 depends on Python>=3.9, we can conclude that sphinx==7.2.6 cannot be used. And because only sphinx<=7.2.6 is available, we can conclude that sphinx>=7.2.6 cannot be used. And because pharaohs-tomp:mygroup depends on sphinx>=7.2.6 and your project requires pharaohs-tomp:mygroup, we can conclude that your project's requirements are unsatisfiable.