Support conflict markers in uv export (#11643)

## Summary

Today, if you have a lockfile that includes conflict markers, we write
those markers out to `requirements.txt` in `uv export`. This is
problematic, since no tool will ever evaluate those markers correctly
downstream.

This PR adds handling for the conflict markers, though it's quite
involved. Specifically, we have a new reachability algorithm that
tracks, for each node, the reachable marker for that node _and_ the
marker conditions under which each conflict item is `true` (at that
node).

I'm slightly worried that this algorithm could be wrong for graphs with
cycles, but we only use this logic for lockfiles with conflicts anyway,
so I think it's a strict improvement over the status quo.

Closes https://github.com/astral-sh/uv/issues/11559.

Closes https://github.com/astral-sh/uv/issues/11548.
This commit is contained in:
Charlie Marsh 2025-02-20 12:19:46 -08:00 committed by GitHub
parent 18a75df8bc
commit 5f6529a69a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 796 additions and 49 deletions

View file

@ -18,10 +18,14 @@ use crate::universal_marker::UniversalMarker;
/// marker), we re-queue the node and update all its children. This implicitly handles cycles,
/// whenever we re-reach a node through a cycle the marker we have is a more
/// specific marker/longer path, so we don't update the node and don't re-queue it.
pub(crate) fn marker_reachability<Node, Edge: Reachable + Copy + PartialEq>(
pub(crate) fn marker_reachability<
Marker: Boolean + Copy + PartialEq,
Node,
Edge: Reachable<Marker>,
>(
graph: &Graph<Node, Edge>,
fork_markers: &[Edge],
) -> FxHashMap<NodeIndex, Edge> {
) -> FxHashMap<NodeIndex, Marker> {
// Note that we build including the virtual packages due to how we propagate markers through
// the graph, even though we then only read the markers for base packages.
let mut reachability = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
@ -47,8 +51,8 @@ pub(crate) fn marker_reachability<Node, Edge: Reachable + Copy + PartialEq>(
} else {
fork_markers
.iter()
.fold(Edge::false_marker(), |mut acc, marker| {
acc.or(*marker);
.fold(Edge::false_marker(), |mut acc, edge| {
acc.or(edge.marker());
acc
})
};
@ -62,7 +66,7 @@ pub(crate) fn marker_reachability<Node, Edge: Reachable + Copy + PartialEq>(
let marker = reachability[&parent_index];
for child_edge in graph.edges_directed(parent_index, Direction::Outgoing) {
// The marker for all paths to the child through the parent.
let mut child_marker = *child_edge.weight();
let mut child_marker = child_edge.weight().marker();
child_marker.and(marker);
match reachability.entry(child_edge.target()) {
Entry::Occupied(mut existing) => {
@ -283,14 +287,47 @@ pub(crate) fn simplify_conflict_markers(
}
}
/// A trait for types that can be used as markers in the dependency graph.
pub(crate) trait Reachable {
pub(crate) trait Reachable<T> {
/// The marker representing the "true" value.
fn true_marker() -> Self;
fn true_marker() -> T;
/// The marker representing the "false" value.
fn false_marker() -> Self;
fn false_marker() -> T;
/// The marker attached to the edge.
fn marker(&self) -> T;
}
impl Reachable<MarkerTree> for MarkerTree {
fn true_marker() -> MarkerTree {
MarkerTree::TRUE
}
fn false_marker() -> MarkerTree {
MarkerTree::FALSE
}
fn marker(&self) -> MarkerTree {
*self
}
}
impl Reachable<UniversalMarker> for UniversalMarker {
fn true_marker() -> UniversalMarker {
UniversalMarker::TRUE
}
fn false_marker() -> UniversalMarker {
UniversalMarker::FALSE
}
fn marker(&self) -> UniversalMarker {
*self
}
}
/// A trait for types that can be used as markers in the dependency graph.
pub(crate) trait Boolean {
/// Perform a logical AND operation with another marker.
fn and(&mut self, other: Self);
@ -298,15 +335,7 @@ pub(crate) trait Reachable {
fn or(&mut self, other: Self);
}
impl Reachable for UniversalMarker {
fn true_marker() -> Self {
Self::TRUE
}
fn false_marker() -> Self {
Self::FALSE
}
impl Boolean for UniversalMarker {
fn and(&mut self, other: Self) {
self.and(other);
}
@ -316,15 +345,7 @@ impl Reachable for UniversalMarker {
}
}
impl Reachable for MarkerTree {
fn true_marker() -> Self {
Self::TRUE
}
fn false_marker() -> Self {
Self::FALSE
}
impl Boolean for MarkerTree {
fn and(&mut self, other: Self) {
self.and(other);
}

View file

@ -5,8 +5,10 @@ use std::fmt::Formatter;
use std::path::{Component, Path, PathBuf};
use either::Either;
use petgraph::graph::NodeIndex;
use petgraph::prelude::EdgeRef;
use petgraph::visit::IntoNodeReferences;
use petgraph::Graph;
use petgraph::{Direction, Graph};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use url::Url;
@ -14,12 +16,13 @@ use uv_configuration::{DevGroupsManifest, EditableMode, ExtrasSpecification, Ins
use uv_distribution_filename::{DistExtension, SourceDistExtension};
use uv_fs::Simplified;
use uv_git_types::GitReference;
use uv_normalize::{ExtraName, PackageName};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep508::MarkerTree;
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
use uv_pypi_types::{ConflictItem, ParsedArchiveUrl, ParsedGitUrl};
use crate::graph_ops::marker_reachability;
use crate::graph_ops::{marker_reachability, Reachable};
use crate::lock::{Package, PackageId, Source};
use crate::universal_marker::resolve_conflicts;
use crate::{Installable, LockError};
/// An export of a [`Lock`] that renders in `requirements.txt` format.
@ -41,13 +44,18 @@ impl<'lock> RequirementsTxtExport<'lock> {
install_options: &'lock InstallOptions,
) -> Result<Self, LockError> {
let size_guess = target.lock().packages.len();
let mut petgraph = Graph::with_capacity(size_guess, size_guess);
let mut graph = Graph::<Node<'lock>, Edge<'lock>>::with_capacity(size_guess, size_guess);
let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
let mut seen = FxHashSet::default();
let mut conflicts = if target.lock().conflicts.is_empty() {
None
} else {
Some(FxHashMap::default())
};
let root = petgraph.add_node(Node::Root);
let root = graph.add_node(Node::Root);
// Add the workspace packages to the queue.
for root_name in target.roots() {
@ -64,33 +72,49 @@ impl<'lock> RequirementsTxtExport<'lock> {
if dev.prod() {
// Add the workspace package to the graph.
if let Entry::Vacant(entry) = inverse.entry(&dist.id) {
entry.insert(petgraph.add_node(Node::Package(dist)));
entry.insert(graph.add_node(Node::Package(dist)));
}
// Add an edge from the root.
let index = inverse[&dist.id];
petgraph.add_edge(root, index, MarkerTree::TRUE);
graph.add_edge(root, index, Edge::Prod(MarkerTree::TRUE));
// Push its dependencies on the queue.
queue.push_back((dist, None));
for extra in extras.extra_names(dist.optional_dependencies.keys()) {
queue.push_back((dist, Some(extra)));
// Track the activated extra in the list of known conflicts.
if let Some(conflicts) = conflicts.as_mut() {
conflicts.insert(
ConflictItem::from((dist.id.name.clone(), extra.clone())),
MarkerTree::TRUE,
);
}
}
}
// Add any development dependencies.
for dep in dist
for (group, dep) in dist
.dependency_groups
.iter()
.filter_map(|(group, deps)| {
if dev.contains(group) {
Some(deps)
Some(deps.iter().map(move |dep| (group, dep)))
} else {
None
}
})
.flatten()
{
// Track the activated group in the list of known conflicts.
if let Some(conflicts) = conflicts.as_mut() {
conflicts.insert(
ConflictItem::from((dist.id.name.clone(), group.clone())),
MarkerTree::TRUE,
);
}
if prune.contains(&dep.package_id.name) {
continue;
}
@ -99,17 +123,17 @@ impl<'lock> RequirementsTxtExport<'lock> {
// Add the dependency to the graph.
if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) {
entry.insert(petgraph.add_node(Node::Package(dep_dist)));
entry.insert(graph.add_node(Node::Package(dep_dist)));
}
// Add an edge from the root. Development dependencies may be installed without
// installing the workspace package itself (which can never have markers on it
// anyway), so they're directly connected to the root.
let dep_index = inverse[&dep.package_id];
petgraph.add_edge(
graph.add_edge(
root,
dep_index,
dep.simplified_marker.as_simplified_marker_tree(),
Edge::Dev(group, dep.simplified_marker.as_simplified_marker_tree()),
);
// Push its dependencies on the queue.
@ -189,12 +213,12 @@ impl<'lock> RequirementsTxtExport<'lock> {
// Add the dependency to the graph.
if let Entry::Vacant(entry) = inverse.entry(&dist.id) {
entry.insert(petgraph.add_node(Node::Package(dist)));
entry.insert(graph.add_node(Node::Package(dist)));
}
// Add an edge from the root.
let dep_index = inverse[&dist.id];
petgraph.add_edge(root, dep_index, marker);
graph.add_edge(root, dep_index, Edge::Prod(marker));
// Push its dependencies on the queue.
if seen.insert((&dist.id, None)) {
@ -230,19 +254,24 @@ impl<'lock> RequirementsTxtExport<'lock> {
continue;
}
// Evaluate the conflict marker.
let dep_dist = target.lock().find_by_id(&dep.package_id);
// Add the dependency to the graph.
if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) {
entry.insert(petgraph.add_node(Node::Package(dep_dist)));
entry.insert(graph.add_node(Node::Package(dep_dist)));
}
// Add the edge.
let dep_index = inverse[&dep.package_id];
petgraph.add_edge(
graph.add_edge(
index,
dep_index,
dep.simplified_marker.as_simplified_marker_tree(),
if let Some(extra) = extra {
Edge::Optional(extra, dep.simplified_marker.as_simplified_marker_tree())
} else {
Edge::Prod(dep.simplified_marker.as_simplified_marker_tree())
},
);
// Push its dependencies on the queue.
@ -257,10 +286,15 @@ impl<'lock> RequirementsTxtExport<'lock> {
}
}
let mut reachability = marker_reachability(&petgraph, &[]);
// Determine the reachability of each node in the graph.
let mut reachability = if let Some(conflicts) = conflicts.as_ref() {
conflict_marker_reachability(&graph, &[], conflicts)
} else {
marker_reachability(&graph, &[])
};
// Collect all packages.
let mut nodes = petgraph
let mut nodes = graph
.node_references()
.filter_map(|(index, node)| match node {
Node::Root => None,
@ -277,6 +311,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
package,
marker: reachability.remove(&index).unwrap_or_default(),
})
.filter(|requirement| !requirement.marker.is_false())
.collect::<Vec<_>>();
// Sort the nodes, such that unnamed URLs (editables) appear at the top.
@ -292,6 +327,170 @@ impl<'lock> RequirementsTxtExport<'lock> {
}
}
/// Determine the markers under which a package is reachable in the dependency tree, taking into
/// account conflicts.
///
/// This method is structurally similar to [`marker_reachability`], but it _also_ attempts to resolve
/// conflict markers. Specifically, in addition to tracking the reachability marker for each node,
/// we also track (for each node) the conditions under which each conflict item is `true`. Then,
/// when evaluating the marker for the node, we inline the conflict marker conditions, thus removing
/// all conflict items from the marker expression.
fn conflict_marker_reachability<'lock>(
graph: &Graph<Node<'lock>, Edge<'lock>>,
fork_markers: &[Edge<'lock>],
known_conflicts: &FxHashMap<ConflictItem, MarkerTree>,
) -> FxHashMap<NodeIndex, MarkerTree> {
// For each node, track the conditions under which each conflict item is enabled.
let mut conflict_maps =
FxHashMap::<NodeIndex, FxHashMap<ConflictItem, MarkerTree>>::with_capacity_and_hasher(
graph.node_count(),
FxBuildHasher,
);
// Note that we build including the virtual packages due to how we propagate markers through
// the graph, even though we then only read the markers for base packages.
let mut reachability = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
// Collect the root nodes.
//
// Besides the actual virtual root node, virtual dev dependencies packages are also root
// nodes since the edges don't cover dev dependencies.
let mut queue: Vec<_> = graph
.node_indices()
.filter(|node_index| {
graph
.edges_directed(*node_index, Direction::Incoming)
.next()
.is_none()
})
.collect();
// The root nodes are always applicable, unless the user has restricted resolver
// environments with `tool.uv.environments`.
let root_markers = if fork_markers.is_empty() {
MarkerTree::TRUE
} else {
fork_markers
.iter()
.fold(MarkerTree::FALSE, |mut acc, edge| {
acc.or(*edge.marker());
acc
})
};
for root_index in &queue {
reachability.insert(*root_index, root_markers);
}
// Propagate all markers through the graph, so that the eventual marker for each node is the
// union of the markers of each path we can reach the node by.
while let Some(parent_index) = queue.pop() {
// Resolve any conflicts in the parent marker.
reachability.entry(parent_index).and_modify(|marker| {
let conflict_map = conflict_maps.get(&parent_index).unwrap_or(known_conflicts);
*marker = resolve_conflicts(*marker, conflict_map);
});
// When we see an edge like `parent [dotenv]> flask`, we should take the reachability
// on `parent`, combine it with the marker on the edge, then add `flask[dotenv]` to
// the inference map on the `flask` node.
for child_edge in graph.edges_directed(parent_index, Direction::Outgoing) {
let mut parent_marker = reachability[&parent_index];
// The marker for all paths to the child through the parent.
let mut parent_map = conflict_maps
.get(&parent_index)
.cloned()
.unwrap_or_else(|| known_conflicts.clone());
match child_edge.weight() {
Edge::Prod(marker) => {
// Resolve any conflicts on the edge.
let marker = resolve_conflicts(*marker, &parent_map);
// Propagate the edge to the known conflicts.
for value in parent_map.values_mut() {
value.and(marker);
}
// Propagate the edge to the node itself.
parent_marker.and(marker);
}
Edge::Optional(extra, marker) => {
// Resolve any conflicts on the edge.
let marker = resolve_conflicts(*marker, &parent_map);
// Propagate the edge to the known conflicts.
for value in parent_map.values_mut() {
value.and(marker);
}
// Propagate the edge to the node itself.
parent_marker.and(marker);
// Add a known conflict item for the extra.
if let Node::Package(parent) = graph[parent_index] {
let item = ConflictItem::from((parent.name().clone(), (*extra).clone()));
parent_map.insert(item, parent_marker);
}
}
Edge::Dev(group, marker) => {
// Resolve any conflicts on the edge.
let marker = resolve_conflicts(*marker, &parent_map);
// Propagate the edge to the known conflicts.
for value in parent_map.values_mut() {
value.and(marker);
}
// Propagate the edge to the node itself.
parent_marker.and(marker);
// Add a known conflict item for the group.
if let Node::Package(parent) = graph[parent_index] {
let item = ConflictItem::from((parent.name().clone(), (*group).clone()));
parent_map.insert(item, parent_marker);
}
}
}
// Combine the inferred conflicts with the existing conflicts on the node.
match conflict_maps.entry(child_edge.target()) {
Entry::Occupied(mut existing) => {
let child_map = existing.get_mut();
for (key, value) in parent_map {
let mut after = child_map.get(&key).copied().unwrap_or(MarkerTree::FALSE);
after.or(value);
child_map.entry(key).or_insert(MarkerTree::FALSE).or(value);
}
}
Entry::Vacant(vacant) => {
vacant.insert(parent_map);
}
}
// Combine the inferred marker with the existing marker on the node.
match reachability.entry(child_edge.target()) {
Entry::Occupied(existing) => {
// If the marker is a subset of the existing marker (A ⊆ B exactly if
// A B = A), updating the child wouldn't change child's marker.
parent_marker.or(*existing.get());
if parent_marker != *existing.get() {
queue.push(child_edge.target());
}
}
Entry::Vacant(vacant) => {
vacant.insert(parent_marker);
queue.push(child_edge.target());
}
}
queue.push(child_edge.target());
}
}
reachability
}
impl std::fmt::Display for RequirementsTxtExport<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// Write out each package.
@ -399,6 +598,39 @@ enum Node<'lock> {
Package(&'lock Package),
}
/// An edge in the resolution graph, along with the marker that must be satisfied to traverse it.
#[derive(Debug, Clone)]
enum Edge<'lock> {
Prod(MarkerTree),
Optional(&'lock ExtraName, MarkerTree),
Dev(&'lock GroupName, MarkerTree),
}
impl Edge<'_> {
/// Return the [`MarkerTree`] for this edge.
fn marker(&self) -> &MarkerTree {
match self {
Self::Prod(marker) => marker,
Self::Optional(_, marker) => marker,
Self::Dev(_, marker) => marker,
}
}
}
impl Reachable<MarkerTree> for Edge<'_> {
fn true_marker() -> MarkerTree {
MarkerTree::TRUE
}
fn false_marker() -> MarkerTree {
MarkerTree::FALSE
}
fn marker(&self) -> MarkerTree {
*self.marker()
}
}
/// A flat requirement, with its associated marker.
#[derive(Debug, Clone, PartialEq, Eq)]
struct Requirement<'lock> {

View file

@ -1,9 +1,12 @@
use std::borrow::Borrow;
use itertools::Itertools;
use rustc_hash::FxHashMap;
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder, MarkerOperator, MarkerTree};
use uv_pep508::{
ExtraOperator, MarkerEnvironment, MarkerEnvironmentBuilder, MarkerExpression, MarkerOperator,
MarkerTree,
};
use uv_pypi_types::{ConflictItem, ConflictPackage, Conflicts};
use crate::ResolveError;
@ -616,6 +619,110 @@ impl<'a> ParsedRawExtra<'a> {
}
}
/// Resolve the conflict markers in a [`MarkerTree`] based on the conditions under which each
/// conflict item is known to be true.
///
/// For example, if the `cpu` extra is known to be enabled when `sys_platform == 'darwin'`, then
/// given the combined marker `python_version >= '3.8' and extra == 'extra-7-project-cpu'`, this
/// method would return `python_version >= '3.8' and sys_platform == 'darwin'`.
///
/// If a conflict item isn't present in the map of known conflicts, it's assumed to be false in all
/// environments.
pub(crate) fn resolve_conflicts(
marker: MarkerTree,
known_conflicts: &FxHashMap<ConflictItem, MarkerTree>,
) -> MarkerTree {
if marker.is_true() || marker.is_false() {
return marker;
}
let mut transformed = MarkerTree::FALSE;
// Convert the marker to DNF, then re-build it.
for dnf in marker.to_dnf() {
let mut or = MarkerTree::TRUE;
for marker in dnf {
let MarkerExpression::Extra {
ref operator,
ref name,
} = marker
else {
or.and(MarkerTree::expression(marker));
continue;
};
let Some(name) = name.as_extra() else {
or.and(MarkerTree::expression(marker));
continue;
};
// Given an extra marker (like `extra == 'extra-7-project-cpu'`), search for the
// corresponding conflict; once found, inline the marker of conditions under which the
// conflict is known to be true.
let mut found = false;
for (conflict_item, conflict_marker) in known_conflicts {
// Search for the conflict item as an extra.
if let Some(extra) = conflict_item.extra() {
let package = conflict_item.package();
let encoded = encode_package_extra(package, extra);
if encoded == *name {
match operator {
ExtraOperator::Equal => {
or.and(*conflict_marker);
found = true;
break;
}
ExtraOperator::NotEqual => {
or.and(conflict_marker.negate());
found = true;
break;
}
}
}
}
// Search for the conflict item as a group.
if let Some(group) = conflict_item.group() {
let package = conflict_item.package();
let encoded = encode_package_group(package, group);
if encoded == *name {
match operator {
ExtraOperator::Equal => {
or.and(*conflict_marker);
found = true;
break;
}
ExtraOperator::NotEqual => {
or.and(conflict_marker.negate());
found = true;
break;
}
}
}
}
}
// If we didn't find the marker in the list of known conflicts, assume it's always
// false.
if !found {
match operator {
ExtraOperator::Equal => {
or.and(MarkerTree::FALSE);
}
ExtraOperator::NotEqual => {
or.and(MarkerTree::TRUE);
}
}
}
}
transformed.or(or);
}
transformed
}
#[cfg(test)]
mod tests {
use super::*;
@ -661,6 +768,25 @@ mod tests {
ConflictMarker::extra(&create_package("pkg"), &create_extra(name))
}
/// Shortcut for creating a conflict item from an extra name.
fn create_extra_item(name: &str) -> ConflictItem {
ConflictItem::from((create_package("pkg"), create_extra(name)))
}
/// Shortcut for creating a conflict map.
fn create_known_conflicts<'a>(
it: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> FxHashMap<ConflictItem, MarkerTree> {
it.into_iter()
.map(|(extra, marker)| {
(
create_extra_item(extra),
MarkerTree::from_str(marker).unwrap(),
)
})
.collect()
}
/// Returns a string representation of the given conflict marker.
///
/// This is just the underlying marker. And if it's `true`, then a
@ -781,4 +907,28 @@ mod tests {
dep_conflict_marker.imbibe(conflicts_marker);
assert_eq!(format!("{dep_conflict_marker:?}"), "true");
}
#[test]
fn resolve() {
let known_conflicts = create_known_conflicts([("foo", "sys_platform == 'darwin'")]);
let cm = MarkerTree::from_str("(python_version >= '3.10' and extra == 'extra-3-pkg-foo') or (python_version < '3.10' and extra != 'extra-3-pkg-foo')").unwrap();
let cm = resolve_conflicts(cm, &known_conflicts);
assert_eq!(
cm.try_to_string().as_deref(),
Some("(python_full_version < '3.10' and sys_platform != 'darwin') or (python_full_version >= '3.10' and sys_platform == 'darwin')")
);
let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-foo'")
.unwrap();
let cm = resolve_conflicts(cm, &known_conflicts);
assert_eq!(
cm.try_to_string().as_deref(),
Some("python_full_version >= '3.10' and sys_platform == 'darwin'")
);
let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-bar'")
.unwrap();
let cm = resolve_conflicts(cm, &known_conflicts);
assert!(cm.is_false());
}
}

View file

@ -2424,3 +2424,347 @@ fn conflicts() -> Result<()> {
Ok(())
}
#[test]
fn simple_conflict_markers() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12.0"
dependencies = ["anyio"]
[project.optional-dependencies]
cpu = [
"idna<=1",
]
cu124 = [
"idna<=2",
]
[tool.uv]
conflicts = [
[
{ extra = "cpu" },
{ extra = "cu124" },
],
]
"#,
)?;
context.lock().assert().success();
uv_snapshot!(context.filters(), context.export(), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv export --cache-dir [CACHE_DIR]
anyio==1.3.1 \
--hash=sha256:a46bb2b7743455434afd9adea848a3c4e0b7321aee3e9d08844b11d348d3b5a0 \
--hash=sha256:f21b4fafeec1b7db81e09a907e44e374a1e39718d782a488fdfcdcf949c8950c
async-generator==1.10 \
--hash=sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b \
--hash=sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
----- stderr -----
Resolved 6 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.export().arg("--extra").arg("cpu"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv export --cache-dir [CACHE_DIR] --extra cpu
anyio==1.3.1 \
--hash=sha256:a46bb2b7743455434afd9adea848a3c4e0b7321aee3e9d08844b11d348d3b5a0 \
--hash=sha256:f21b4fafeec1b7db81e09a907e44e374a1e39718d782a488fdfcdcf949c8950c
async-generator==1.10 \
--hash=sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b \
--hash=sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144
idna==1.0 \
--hash=sha256:c31140a69ecae014d65e936e9a45d8a66e2ee29f5abbc656f69c705ad2f1507d
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
----- stderr -----
Resolved 6 packages in [TIME]
"###);
Ok(())
}
#[test]
fn complex_conflict_markers() -> Result<()> {
let context = TestContext::new("3.12").with_exclude_newer("2025-01-30T00:00:00Z");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12.0"
dependencies = ["torch"]
[project.optional-dependencies]
cpu = [
"torch>=2.6.0",
"torchvision>=0.21.0",
]
cu124 = [
"torch>=2.6.0",
"torchvision>=0.21.0",
]
[tool.uv]
conflicts = [
[
{ extra = "cpu" },
{ extra = "cu124" },
],
]
[tool.uv.sources]
torch = [
{ index = "pytorch-cpu", extra = "cpu" },
{ index = "pytorch-cu124", extra = "cu124" },
]
torchvision = [
{ index = "pytorch-cpu", extra = "cpu" },
{ index = "pytorch-cu124", extra = "cu124" },
]
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://astral-sh.github.io/pytorch-mirror/whl/cpu"
explicit = true
[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://astral-sh.github.io/pytorch-mirror/whl/cu124"
explicit = true
"#,
)?;
context.lock().assert().success();
uv_snapshot!(context.filters(), context.export(), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv export --cache-dir [CACHE_DIR]
nvidia-cublas-cu12==12.4.5.8 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3 \
--hash=sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b \
--hash=sha256:5a796786da89203a0657eda402bcdcec6180254a8ac22d72213abc42069522dc
nvidia-cuda-cupti-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:5688d203301ab051449a2b1cb6690fbe90d2b372f411521c86018b950f3d7922 \
--hash=sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a \
--hash=sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb
nvidia-cuda-nvrtc-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:0eedf14185e04b76aa05b1fea04133e59f465b6f960c0cbf4e37c3cb6b0ea198 \
--hash=sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338 \
--hash=sha256:a961b2f1d5f17b14867c619ceb99ef6fcec12e46612711bcec78eb05068a60ec
nvidia-cuda-runtime-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:09c2e35f48359752dfa822c09918211844a3d93c100a715d79b59591130c5e1e \
--hash=sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5 \
--hash=sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3
nvidia-cudnn-cu12==9.1.0.70 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f \
--hash=sha256:6278562929433d68365a07a4a1546c237ba2849852c0d4b2262a486e805b977a
nvidia-cufft-cu12==11.2.1.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399 \
--hash=sha256:d802f4954291101186078ccbe22fc285a902136f974d369540fd4a5333d1440b \
--hash=sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9
nvidia-curand-cu12==10.3.5.147 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:1f173f09e3e3c76ab084aba0de819c49e56614feae5c12f69883f4ae9bb5fad9 \
--hash=sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b \
--hash=sha256:f307cc191f96efe9e8f05a87096abc20d08845a841889ef78cb06924437f6771
nvidia-cusolver-cu12==11.6.1.9 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260 \
--hash=sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e \
--hash=sha256:e77314c9d7b694fcebc84f58989f3aa4fb4cb442f12ca1a9bde50f5e8f6d1b9c
nvidia-cusparse-cu12==12.3.1.170 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:9bc90fb087bc7b4c15641521f31c0371e9a612fc2ba12c338d3ae032e6b6797f \
--hash=sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3 \
--hash=sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1
nvidia-cusparselt-cu12==0.6.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:0057c91d230703924c0422feabe4ce768841f9b4b44d28586b6f6d2eb86fbe70 \
--hash=sha256:067a7f6d03ea0d4841c85f0c6f1991c5dda98211f6302cb83a4ab234ee95bef8 \
--hash=sha256:df2c24502fd76ebafe7457dbc4716b2fec071aabaed4fb7691a201cde03704d9
nvidia-nccl-cu12==2.21.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0
nvidia-nvjitlink-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57 \
--hash=sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83 \
--hash=sha256:fd9020c501d27d135f983c6d3e244b197a7ccad769e34df53a42e276b0e25fa1
nvidia-nvtx-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:641dccaaa1139f3ffb0d3164b4b84f9d253397e38246a4f2f36728b48566d485 \
--hash=sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a \
--hash=sha256:7959ad635db13edf4fc65c06a6e9f9e55fc2f92596db928d169c0bb031e88ef3
torch==2.6.0 \
--hash=sha256:2bb8987f3bb1ef2675897034402373ddfc8f5ef0e156e2d8cfc47cacafdda4a9 \
--hash=sha256:4874a73507a300a5d089ceaff616a569e7bb7c613c56f37f63ec3ffac65259cf \
--hash=sha256:510c73251bee9ba02ae1cb6c9d4ee0907b3ce6020e62784e2d7598e0cfa4d6cc \
--hash=sha256:7e1448426d0ba3620408218b50aa6ada88aeae34f7a239ba5431f6c8774b1239 \
--hash=sha256:9a610afe216a85a8b9bc9f8365ed561535c93e804c2a317ef7fabcc5deda0989 \
--hash=sha256:a0d5e1b9874c1a6c25556840ab8920569a7a4137afa8a63a32cee0bc7d89bd4b \
--hash=sha256:b789069020c5588c70d5c2158ac0aa23fd24a028f34a8b4fcb8fcb4d7efcf5fb \
--hash=sha256:ff96f4038f8af9f7ec4231710ed4549da1bdebad95923953a25045dcf6fd87e2
triton==3.2.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \
--hash=sha256:8d9b215efc1c26fa7eefb9a157915c92d52e000d2bf83e5f69704047e63f125c \
--hash=sha256:e5dfa23ba84541d7c0a531dfce76d8bcd19159d50a4a8b14ad01e91734a5c1b0
----- stderr -----
Resolved 33 packages in [TIME]
"###);
uv_snapshot!(context.filters(), context.export().arg("--extra").arg("cpu"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv export --cache-dir [CACHE_DIR] --extra cpu
filelock==3.17.0 ; sys_platform == 'darwin' \
--hash=sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338 \
--hash=sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e
fsspec==2024.12.0 ; sys_platform == 'darwin' \
--hash=sha256:670700c977ed2fb51e0d9f9253177ed20cbde4a3e5c0283cc5385b5870c8533f \
--hash=sha256:b520aed47ad9804237ff878b504267a3b0b441e97508bd6d2d8774e3db85cee2
jinja2==3.1.5 ; sys_platform == 'darwin' \
--hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \
--hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb
markupsafe==3.0.2 ; sys_platform == 'darwin' \
--hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
--hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
--hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
--hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
--hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
--hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
--hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
--hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
--hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
--hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
--hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
--hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
--hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
--hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
--hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
--hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
--hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
--hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
--hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
--hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
--hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
--hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
--hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
--hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
--hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
--hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
--hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
--hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
--hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
--hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
--hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430
mpmath==1.3.0 ; sys_platform == 'darwin' \
--hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \
--hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c
networkx==3.4.2 ; sys_platform == 'darwin' \
--hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \
--hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f
numpy==2.2.2 ; (platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin' \
--hash=sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0 \
--hash=sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2 \
--hash=sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4 \
--hash=sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648 \
--hash=sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be \
--hash=sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb \
--hash=sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd \
--hash=sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a \
--hash=sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84 \
--hash=sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748 \
--hash=sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825 \
--hash=sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317 \
--hash=sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283 \
--hash=sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278 \
--hash=sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9 \
--hash=sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de \
--hash=sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369 \
--hash=sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb \
--hash=sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49 \
--hash=sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37 \
--hash=sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39 \
--hash=sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576 \
--hash=sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba \
--hash=sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7 \
--hash=sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467 \
--hash=sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc \
--hash=sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391 \
--hash=sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0 \
--hash=sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369 \
--hash=sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff \
--hash=sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f
pillow==11.1.0 ; (platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin' \
--hash=sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65 \
--hash=sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a \
--hash=sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352 \
--hash=sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20 \
--hash=sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c \
--hash=sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114 \
--hash=sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91 \
--hash=sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5 \
--hash=sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c \
--hash=sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756 \
--hash=sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861 \
--hash=sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1 \
--hash=sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a \
--hash=sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081 \
--hash=sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5 \
--hash=sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1 \
--hash=sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3 \
--hash=sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f \
--hash=sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c \
--hash=sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf \
--hash=sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b \
--hash=sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe \
--hash=sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc \
--hash=sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec \
--hash=sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3 \
--hash=sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0 \
--hash=sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6 \
--hash=sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547 \
--hash=sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9 \
--hash=sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab \
--hash=sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9
setuptools==75.8.0 ; sys_platform == 'darwin' \
--hash=sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6 \
--hash=sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3
sympy==1.13.1 ; sys_platform == 'darwin' \
--hash=sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f \
--hash=sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8
torch==2.6.0 ; sys_platform == 'darwin'
torch==2.6.0+cpu ; sys_platform != 'darwin'
torchvision==0.21.0 ; (platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin'
torchvision==0.21.0+cpu ; (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')
typing-extensions==4.12.2 ; sys_platform == 'darwin' \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
----- stderr -----
Resolved 33 packages in [TIME]
"###);
Ok(())
}