Avoid invalid simplification with conflict markers (#15041)

Previously, `simplify_conflict_markers` assumed that it can remove all
conflict set together, when we need to look at each conflict set
individually. Specifically, `(platform_machine == 'x86_64' and extra ==
'extra-5-foo-b') or extra == 'extra-5-foo-a'` can't be reduced
`platform_machine == 'x86_64'` only because it reduces to true when both
conflict extras are activated.

This case applied in https://github.com/astral-sh/uv/issues/14805, where
a jax 0.5.3 version was used for `platform_machine != 'aarch64' or
sys_platform != 'linux'` and the conflict extra `cu128`, but jax 0.7.0
for the conflict extra `cpu`.

Only removing the faulty inference regresses lockfiles to much more
verbose markers. To balance the much more conservative inference, I
added `unify_inference_sets` to simplify cases where all conflict
branches reduce to the same marker.

This still regresses some markers. For example `sys_platform == 'win32'`
regresses to `sys_platform == 'win32' or (extra == 'extra-3-pkg-x1' and
extra == 'extra-3-pkg-x2')` in `extra_inferences`, even through x1 and
x2 conflict and the second conjunction could be simplified away.

Fixes https://github.com/astral-sh/uv/issues/14805
This commit is contained in:
konsti 2025-08-06 11:26:26 +02:00 committed by GitHub
parent ce37286814
commit 91653f5fee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 419 additions and 127 deletions

View File

@ -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()]));
}
}

View File

@ -756,3 +756,24 @@ impl From<SchemaConflictItem> 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,
}

View File

@ -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<ResolutionGraphNode, UniversalMarker>,
) {
/// 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<NodeIndex, Vec<FxHashSet<Inference>>> = FxHashMap::default();
let mut inferences: FxHashMap<NodeIndex, Vec<BTreeSet<Inference>>> = 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::<Vec<_>>();
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);
}
}
}

View File

@ -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<impl std::fmt::Display + '_> {
match self.kind {
pub(crate) fn end_user_fork_display(&self) -> Option<String> {
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::<BTreeSet<_>>()
.into_iter()
.join(", "),
));
}
if !exclude.is_empty() {
descriptors.push(format!(
"excluded: {}",
// Sort to ensure stable error messages
exclude
.iter()
.map(format_conflict_item)
.collect::<BTreeSet<_>>()
.into_iter()
.join(", "),
));
}
Some(format!("split ({})", descriptors.join("; ")))
}
}
}

View File

@ -708,12 +708,19 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
resolutions.len()
);
}
for resolution in &resolutions {
if let Some(env) = resolution.env.end_user_fork_display() {
debug!(
"Distinct solution for {env} with {} packages",
resolution.nodes.len()
);
if tracing::enabled!(Level::DEBUG) {
for resolution in &resolutions {
if let Some(env) = resolution.env.end_user_fork_display() {
let packages: FxHashSet<_> = resolution
.nodes
.keys()
.map(|package| &package.name)
.collect();
debug!(
"Distinct solution for {env} with {} package(s)",
packages.len()
);
}
}
}
for resolution in &resolutions {

View File

@ -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<Inference>]) {
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::<Vec<ExtraName>>())
}
/// Returns true if the marker always evaluates to true if the given set of extras is activated.
pub(crate) fn evaluate_only_extras<P, E, G>(self, extras: &[(P, E)], groups: &[(P, G)]) -> bool
where
P: Borrow<PackageName>,
E: Borrow<ExtraName>,
G: Borrow<GroupName>,
{
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::<Vec<ExtraName>>())
}
/// 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<P, E, G>(self, extras: &[(P, E)], groups: &[(P, G)]) -> bool
where
P: Borrow<PackageName>,
E: Borrow<ExtraName>,
G: Borrow<GroupName>,
{
static DUMMY: std::sync::LazyLock<MarkerEnvironment> = 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::<Vec<ExtraName>>())
}
/// 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::<Vec<(PackageName, ExtraName)>>();
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::<Vec<(PackageName, ExtraName)>>();
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:?}`"
);
}

View File

@ -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

View File

@ -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: <https://github.com/astral-sh/uv/issues/14805>
#[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(())
}

View File

@ -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.

View File

@ -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.

View File

@ -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.