diff --git a/crates/uv-resolver/src/lock/export/mod.rs b/crates/uv-resolver/src/lock/export/mod.rs index 25cafd363b0c7..e2bcffa6a4269 100644 --- a/crates/uv-resolver/src/lock/export/mod.rs +++ b/crates/uv-resolver/src/lock/export/mod.rs @@ -15,13 +15,13 @@ use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep508::MarkerTree; use uv_pypi_types::ConflictItem; -use crate::graph_ops::{Reachable, marker_reachability}; +use crate::graph_ops::Reachable; use crate::lock::LockErrorKind; pub use crate::lock::export::metadata::Metadata; pub(crate) use crate::lock::export::pylock_toml::PylockTomlPackage; pub use crate::lock::export::pylock_toml::{PylockToml, PylockTomlErrorKind}; pub use crate::lock::export::requirements_txt::RequirementsTxtExport; -use crate::universal_marker::resolve_conflicts; +use crate::universal_marker::resolve_activated_extras; use crate::{Installable, LockError, Package}; pub mod cyclonedx_json; @@ -57,16 +57,10 @@ impl<'lock> ExportableRequirements<'lock> { let size_guess = target.lock().packages.len(); let mut graph = Graph::, Edge<'lock>>::with_capacity(size_guess, size_guess); let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher); - let mut selected_extras: FxHashMap<_, Vec> = - 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 mut activated_items = FxHashMap::default(); let root = graph.add_node(Node::Root); @@ -87,39 +81,33 @@ impl<'lock> ExportableRequirements<'lock> { })?; // Track the activated package in the list of known conflicts. - if let Some(conflicts) = conflicts.as_mut() { - conflicts.insert(ConflictItem::from(dist.id.name.clone()), MarkerTree::TRUE); - } + activated_items.insert(ConflictItem::from(dist.id.name.clone()), MarkerTree::TRUE); if groups.prod() { // Add the workspace package to the graph. let index = *inverse .entry(&dist.id) .or_insert_with(|| graph.add_node(Node::Package(dist))); - graph.add_edge(root, index, Edge::Prod(MarkerTree::TRUE)); + graph.add_edge( + root, + index, + Edge::Prod { + marker: MarkerTree::TRUE, + dep_extras: Vec::new(), + }, + ); // Track the activated project in the list of known conflicts. - if let Some(conflicts) = conflicts.as_mut() { - conflicts.insert(ConflictItem::from(dist.id.name.clone()), MarkerTree::TRUE); - } + activated_items.insert(ConflictItem::from(dist.id.name.clone()), MarkerTree::TRUE); // Push its dependencies on the queue. queue.push_back((dist, None)); - let mut root_extras = Vec::new(); for extra in extras.extra_names(dist.optional_dependencies.keys()) { - root_extras.push(extra.clone()); 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, - ); - } - } - if !root_extras.is_empty() { - selected_extras.insert(&dist.id, root_extras); + activated_items.insert( + ConflictItem::from((dist.id.name.clone(), extra.clone())), + MarkerTree::TRUE, + ); } } @@ -137,12 +125,10 @@ impl<'lock> ExportableRequirements<'lock> { .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, - ); - } + activated_items.insert( + ConflictItem::from((dist.id.name.clone(), group.clone())), + MarkerTree::TRUE, + ); if prune.contains(&dep.package_id.name) { continue; @@ -161,7 +147,11 @@ impl<'lock> ExportableRequirements<'lock> { graph.add_edge( root, dep_index, - Edge::Dev(group, dep.simplified_marker.as_simplified_marker_tree()), + Edge::Dev { + group, + marker: dep.simplified_marker.as_simplified_marker_tree(), + dep_extras: dep.extra.iter().collect(), + }, ); // Push its dependencies on the queue. @@ -245,7 +235,14 @@ impl<'lock> ExportableRequirements<'lock> { .or_insert_with(|| graph.add_node(Node::Package(dist))); // Add an edge from the root. - graph.add_edge(root, dep_index, Edge::Prod(marker)); + graph.add_edge( + root, + dep_index, + Edge::Prod { + marker, + dep_extras: requirement.extras.iter().collect(), + }, + ); // Push its dependencies on the queue. if seen.insert((&dist.id, None)) { @@ -289,50 +286,21 @@ impl<'lock> ExportableRequirements<'lock> { .entry(&dep.package_id) .or_insert_with(|| graph.add_node(Node::Package(dep_dist))); - // Add an edge from the dependency. - let mut active_extras = selected_extras - .get(&package.id) - .cloned() - .unwrap_or_default(); - if let Some(extra) = extra { - if !active_extras.contains(extra) { - active_extras.push((*extra).clone()); - } - } - if !active_extras.is_empty() { - match selected_extras.entry(&dep.package_id) { - Entry::Occupied(mut entry) => { - for extra in &active_extras { - if !entry.get().contains(extra) { - entry.get_mut().push(extra.clone()); - } - } - } - Entry::Vacant(entry) => { - entry.insert(active_extras.clone()); - } - } - } - + let dep_extras = dep.extra.iter().collect::>(); graph.add_edge( index, dep_index, if let Some(extra) = extra { - Edge::Optional( + Edge::Optional { extra, - dep.simplified_marker - .as_simplified_marker_tree() - .simplify_extras(std::slice::from_ref(extra)), - ) + marker: dep.simplified_marker.as_simplified_marker_tree(), + dep_extras, + } } else { - Edge::Prod(selected_extras.get(&package.id).map_or_else( - || dep.simplified_marker.as_simplified_marker_tree(), - |extras| { - dep.simplified_marker - .as_simplified_marker_tree() - .simplify_extras(extras) - }, - )) + Edge::Prod { + marker: dep.simplified_marker.as_simplified_marker_tree(), + dep_extras, + } }, ); @@ -349,11 +317,7 @@ impl<'lock> ExportableRequirements<'lock> { } // 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, &[]) - }; + let mut reachability = conflict_marker_reachability(&graph, &[], &activated_items); // Collect all packages. let nodes = graph @@ -405,18 +369,38 @@ enum Node<'lock> { /// 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), + Prod { + marker: MarkerTree, + dep_extras: Vec<&'lock ExtraName>, + }, + Optional { + extra: &'lock ExtraName, + marker: MarkerTree, + dep_extras: Vec<&'lock ExtraName>, + }, + Dev { + group: &'lock GroupName, + marker: MarkerTree, + dep_extras: Vec<&'lock ExtraName>, + }, } 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, + Self::Prod { marker, .. } => marker, + Self::Optional { marker, .. } => marker, + Self::Dev { marker, .. } => marker, + } + } + + /// Return the dependency extras activated by traversing this edge. + fn dep_extras(&self) -> &[&ExtraName] { + match self { + Self::Prod { dep_extras, .. } => dep_extras, + Self::Optional { dep_extras, .. } => dep_extras, + Self::Dev { dep_extras, .. } => dep_extras, } } } @@ -495,7 +479,11 @@ fn conflict_marker_reachability<'lock>( // 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); + let scope_package = match &graph[parent_index] { + Node::Package(package) => Some(package.name()), + Node::Root => None, + }; + *marker = resolve_activated_extras(*marker, scope_package, conflict_map); }); // When we see an edge like `parent [dotenv]> flask`, we should take the reachability @@ -510,10 +498,22 @@ fn conflict_marker_reachability<'lock>( .cloned() .unwrap_or_else(|| known_conflicts.clone()); + if let Node::Package(child) = graph[child_edge.target()] { + for extra in child_edge.weight().dep_extras() { + let item = ConflictItem::from((child.name().clone(), (*extra).clone())); + parent_map.insert(item, parent_marker); + } + } + + let scope_package = match &graph[parent_index] { + Node::Package(package) => Some(package.name()), + Node::Root => None, + }; + match child_edge.weight() { - Edge::Prod(marker) => { - // Resolve any conflicts on the edge. - let marker = resolve_conflicts(*marker, &parent_map); + Edge::Prod { marker, .. } => { + // Resolve any active extras on the edge. + let marker = resolve_activated_extras(*marker, scope_package, &parent_map); // Propagate the edge to the known conflicts. for value in parent_map.values_mut() { @@ -523,9 +523,16 @@ fn conflict_marker_reachability<'lock>( // 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); + Edge::Optional { extra, marker, .. } => { + // The optional extra is active for this edge itself, so add it before + // resolving any active extras on the edge. + if let Node::Package(parent) = graph[parent_index] { + let item = ConflictItem::from((parent.name().clone(), (*extra).clone())); + parent_map.insert(item, parent_marker); + } + + // Resolve any active extras on the edge. + let marker = resolve_activated_extras(*marker, scope_package, &parent_map); // Propagate the edge to the known conflicts. for value in parent_map.values_mut() { @@ -534,16 +541,17 @@ fn conflict_marker_reachability<'lock>( // Propagate the edge to the node itself. parent_marker.and(marker); - - // Add a known conflict item for the extra. + } + Edge::Dev { group, marker, .. } => { + // The dependency group is active for this edge itself, so add it before + // resolving any active extras on the edge. if let Node::Package(parent) = graph[parent_index] { - let item = ConflictItem::from((parent.name().clone(), (*extra).clone())); + let item = ConflictItem::from((parent.name().clone(), (*group).clone())); parent_map.insert(item, parent_marker); } - } - Edge::Dev(group, marker) => { - // Resolve any conflicts on the edge. - let marker = resolve_conflicts(*marker, &parent_map); + + // Resolve any active extras on the edge. + let marker = resolve_activated_extras(*marker, scope_package, &parent_map); // Propagate the edge to the known conflicts. for value in parent_map.values_mut() { @@ -552,12 +560,6 @@ fn conflict_marker_reachability<'lock>( // 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); - } } } diff --git a/crates/uv-resolver/src/universal_marker.rs b/crates/uv-resolver/src/universal_marker.rs index 09325384372e6..cf26e4e1857e7 100644 --- a/crates/uv-resolver/src/universal_marker.rs +++ b/crates/uv-resolver/src/universal_marker.rs @@ -710,8 +710,15 @@ impl<'a> ParsedRawExtra<'a> { /// /// 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( +/// Resolve unencoded package extra markers and conflict-encoded extra markers in a +/// [`MarkerTree`] based on the conditions under which each item is known to be true. +/// +/// When `scope_package` is set, unencoded package extras like `extra == 'cpu'` are interpreted +/// relative to that package. Conflict-encoded extras and groups are resolved independent of +/// `scope_package`. +pub(crate) fn resolve_activated_extras( marker: MarkerTree, + scope_package: Option<&PackageName>, known_conflicts: &FxHashMap, ) -> MarkerTree { if marker.is_true() || marker.is_false() { @@ -805,6 +812,25 @@ pub(crate) fn resolve_conflicts( } } + // Search for an unencoded package extra in the current package scope. + if !found { + if let Some(package) = scope_package { + let conflict_item = ConflictItem::from((package.clone(), name.clone())); + if let Some(conflict_marker) = known_conflicts.get(&conflict_item) { + match operator { + ExtraOperator::Equal => { + or.and(*conflict_marker); + found = true; + } + ExtraOperator::NotEqual => { + or.and(conflict_marker.negate()); + found = true; + } + } + } + } + } + // If we didn't find the marker in the list of known conflicts, assume it's always // false. if !found { @@ -1014,7 +1040,7 @@ mod tests { 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); + let cm = resolve_activated_extras(cm, None, &known_conflicts); assert_eq!( cm.try_to_string().as_deref(), Some( @@ -1024,7 +1050,7 @@ mod tests { let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-foo'") .unwrap(); - let cm = resolve_conflicts(cm, &known_conflicts); + let cm = resolve_activated_extras(cm, None, &known_conflicts); assert_eq!( cm.try_to_string().as_deref(), Some("python_full_version >= '3.10' and sys_platform == 'darwin'") @@ -1032,7 +1058,31 @@ mod tests { let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-bar'") .unwrap(); - let cm = resolve_conflicts(cm, &known_conflicts); + let cm = resolve_activated_extras(cm, None, &known_conflicts); + assert!(cm.is_false()); + } + + #[test] + fn resolve_unencoded_package_extras() { + let known_conflicts = create_known_conflicts([("foo", "sys_platform == 'darwin'")]); + let package = create_package("pkg"); + + let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'foo'").unwrap(); + let cm = resolve_activated_extras(cm, Some(&package), &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 != 'foo'").unwrap(); + let cm = resolve_activated_extras(cm, Some(&package), &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 == 'bar'").unwrap(); + let cm = resolve_activated_extras(cm, Some(&package), &known_conflicts); assert!(cm.is_false()); } } diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 597062935c04e..96131524efff8 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -3673,6 +3673,214 @@ fn requirements_txt_cyclic_dependencies() -> Result<()> { Ok(()) } +#[test] +fn pylock_workspace_member_conflict_markers() -> Result<()> { + let context = uv_test::test_context!("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 = "root" + version = "0.1.0" + requires-python = ">=3.12.0" + dependencies = ["member"] + + [project.optional-dependencies] + cpu = ["member[cpu]"] + cu124 = ["member[cu124]"] + + [tool.uv.workspace] + members = ["packages/member"] + + [tool.uv.sources] + member = { workspace = true } + "#, + )?; + + let member = context.temp_dir.child("packages/member"); + member.create_dir_all()?; + member.child("pyproject.toml").write_str( + r#" + [project] + name = "member" + version = "0.1.0" + requires-python = ">=3.12.0" + dependencies = [] + + [project.optional-dependencies] + cpu = ["torch>=2.6.0"] + cu124 = ["torch>=2.6.0"] + + [tool.uv] + conflicts = [ + [ + { extra = "cpu" }, + { extra = "cu124" }, + ], + ] + + [tool.uv.sources] + torch = [ + { 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().arg("--format").arg("pylock.toml").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] --format pylock.toml --extra cpu + lock-version = "1.0" + created-by = "uv" + requires-python = ">=3.12.[X]" + + [[packages]] + name = "filelock" + version = "3.17.0" + index = "https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", upload-time = 2025-01-21T20:04:49Z, size = 18027, hashes = { sha256 = "ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e" } } + wheels = [{ url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", upload-time = 2025-01-21T20:04:47Z, size = 16164, hashes = { sha256 = "533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338" } }] + + [[packages]] + name = "fsspec" + version = "2024.12.0" + index = "https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/ee/11/de70dee31455c546fbc88301971ec03c328f3d1138cfba14263f651e9551/fsspec-2024.12.0.tar.gz", upload-time = 2024-12-19T19:57:30Z, size = 291600, hashes = { sha256 = "670700c977ed2fb51e0d9f9253177ed20cbde4a3e5c0283cc5385b5870c8533f" } } + wheels = [{ url = "https://files.pythonhosted.org/packages/de/86/5486b0188d08aa643e127774a99bac51ffa6cf343e3deb0583956dca5b22/fsspec-2024.12.0-py3-none-any.whl", upload-time = 2024-12-19T19:57:28Z, size = 183862, hashes = { sha256 = "b520aed47ad9804237ff878b504267a3b0b441e97508bd6d2d8774e3db85cee2" } }] + + [[packages]] + name = "jinja2" + version = "3.1.5" + index = "https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", upload-time = 2024-12-21T18:30:22Z, size = 244674, hashes = { sha256 = "8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb" } } + wheels = [{ url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", upload-time = 2024-12-21T18:30:19Z, size = 134596, hashes = { sha256 = "aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" } }] + + [[packages]] + name = "markupsafe" + version = "3.0.2" + index = "https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", upload-time = 2024-10-18T15:21:54Z, size = 20537, hashes = { sha256 = "ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0" } } + wheels = [ + { name = "markupsafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", upload-time = 2024-10-18T15:21:13Z, size = 14274, hashes = { sha256 = "9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf" } }, + { name = "markupsafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", upload-time = 2024-10-18T15:21:14Z, size = 12348, hashes = { sha256 = "846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225" } }, + { name = "markupsafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-10-18T15:21:15Z, size = 24149, hashes = { sha256 = "1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028" } }, + { name = "markupsafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-10-18T15:21:17Z, size = 23118, hashes = { sha256 = "e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8" } }, + { name = "markupsafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2024-10-18T15:21:18Z, size = 22993, hashes = { sha256 = "88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c" } }, + { name = "markupsafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", upload-time = 2024-10-18T15:21:18Z, size = 24178, hashes = { sha256 = "2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557" } }, + { name = "markupsafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", upload-time = 2024-10-18T15:21:19Z, size = 23319, hashes = { sha256 = "52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22" } }, + { name = "markupsafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", upload-time = 2024-10-18T15:21:20Z, size = 23352, hashes = { sha256 = "ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48" } }, + { name = "markupsafe-3.0.2-cp312-cp312-win32.whl", url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", upload-time = 2024-10-18T15:21:22Z, size = 15097, hashes = { sha256 = "0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30" } }, + { name = "markupsafe-3.0.2-cp312-cp312-win_amd64.whl", url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", upload-time = 2024-10-18T15:21:23Z, size = 15601, hashes = { sha256 = "8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87" } }, + { name = "markupsafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", upload-time = 2024-10-18T15:21:24Z, size = 14274, hashes = { sha256 = "ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd" } }, + { name = "markupsafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", upload-time = 2024-10-18T15:21:25Z, size = 12352, hashes = { sha256 = "f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430" } }, + { name = "markupsafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-10-18T15:21:26Z, size = 24122, hashes = { sha256 = "569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094" } }, + { name = "markupsafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-10-18T15:21:27Z, size = 23085, hashes = { sha256 = "15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396" } }, + { name = "markupsafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2024-10-18T15:21:27Z, size = 22978, hashes = { sha256 = "f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79" } }, + { name = "markupsafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", upload-time = 2024-10-18T15:21:28Z, size = 24208, hashes = { sha256 = "cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a" } }, + { name = "markupsafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", upload-time = 2024-10-18T15:21:29Z, size = 23357, hashes = { sha256 = "cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca" } }, + { name = "markupsafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", upload-time = 2024-10-18T15:21:30Z, size = 23344, hashes = { sha256 = "444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c" } }, + { name = "markupsafe-3.0.2-cp313-cp313-win32.whl", url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", upload-time = 2024-10-18T15:21:31Z, size = 15101, hashes = { sha256 = "bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1" } }, + { name = "markupsafe-3.0.2-cp313-cp313-win_amd64.whl", url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", upload-time = 2024-10-18T15:21:32Z, size = 15603, hashes = { sha256 = "e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", upload-time = 2024-10-18T15:21:33Z, size = 14510, hashes = { sha256 = "b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", upload-time = 2024-10-18T15:21:34Z, size = 12486, hashes = { sha256 = "a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2024-10-18T15:21:35Z, size = 25480, hashes = { sha256 = "4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-10-18T15:21:36Z, size = 23914, hashes = { sha256 = "c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2024-10-18T15:21:37Z, size = 23796, hashes = { sha256 = "d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", upload-time = 2024-10-18T15:21:37Z, size = 25473, hashes = { sha256 = "6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", upload-time = 2024-10-18T15:21:39Z, size = 24114, hashes = { sha256 = "3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", upload-time = 2024-10-18T15:21:40Z, size = 24098, hashes = { sha256 = "131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-win32.whl", url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", upload-time = 2024-10-18T15:21:41Z, size = 15208, hashes = { sha256 = "ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6" } }, + { name = "markupsafe-3.0.2-cp313-cp313t-win_amd64.whl", url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", upload-time = 2024-10-18T15:21:42Z, size = 15739, hashes = { sha256 = "e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f" } }, + ] + + [[packages]] + name = "member" + directory = { path = "packages/member", editable = true } + + [[packages]] + name = "mpmath" + version = "1.3.0" + index = "https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", upload-time = 2023-03-07T16:47:11Z, size = 508106, hashes = { sha256 = "7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f" } } + wheels = [{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", upload-time = 2023-03-07T16:47:09Z, size = 536198, hashes = { sha256 = "a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c" } }] + + [[packages]] + name = "networkx" + version = "3.4.2" + index = "https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", upload-time = 2024-10-21T12:39:38Z, size = 2151368, hashes = { sha256 = "307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1" } } + wheels = [{ url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", upload-time = 2024-10-21T12:39:36Z, size = 1723263, hashes = { sha256 = "df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f" } }] + + [[packages]] + name = "setuptools" + version = "75.8.0" + index = "https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", upload-time = 2025-01-08T18:28:23Z, size = 1343222, hashes = { sha256 = "c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6" } } + wheels = [{ url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", upload-time = 2025-01-08T18:28:20Z, size = 1228782, hashes = { sha256 = "e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3" } }] + + [[packages]] + name = "sympy" + version = "1.13.1" + index = "https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", upload-time = 2024-07-19T09:26:51Z, size = 7533040, hashes = { sha256 = "9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f" } } + wheels = [{ url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", upload-time = 2024-07-19T09:26:48Z, size = 6189177, hashes = { sha256 = "db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8" } }] + + [[packages]] + name = "torch" + version = "2.6.0" + marker = "sys_platform == 'darwin'" + index = "https://astral-sh.github.io/pytorch-mirror/whl/cpu" + wheels = [ + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0-cp312-none-macosx_11_0_arm64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0-cp313-none-macosx_11_0_arm64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + ] + + [[packages]] + name = "torch" + version = "2.6.0+cpu" + marker = "sys_platform != 'darwin'" + index = "https://astral-sh.github.io/pytorch-mirror/whl/cpu" + wheels = [ + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp312-cp312-linux_x86_64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp312-cp312-win_amd64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp313-cp313-linux_x86_64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp313-cp313-win_amd64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp313-cp313t-linux_x86_64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", upload-time = 2025-01-29T22:50:59Z, hashes = {} }, + ] + + [[packages]] + name = "typing-extensions" + version = "4.12.2" + index = "https://pypi.org/simple" + sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", upload-time = 2024-06-07T18:52:15Z, size = 85321, hashes = { sha256 = "1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" } } + wheels = [{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", upload-time = 2024-06-07T18:52:13Z, size = 37438, hashes = { sha256 = "04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d" } }] + + ----- stderr ----- + Resolved 28 packages in [TIME] + "#); + + Ok(()) +} + /// Export requirements in the presence of a cycle, with conflicts enabled. #[test] fn requirements_txt_cyclic_dependencies_conflict() -> Result<()> {