From cd31e03549b53b5c7d6145463b6a8a58a3c1deeb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 22 Jul 2024 17:05:17 -0400 Subject: [PATCH] Include URLs on graph edges --- crates/uv-resolver/src/resolution/graph.rs | 10 +- crates/uv-resolver/src/resolver/mod.rs | 15 +++ crates/uv/tests/lock.rs | 144 +++++++++++++++++++++ 3 files changed, 167 insertions(+), 2 deletions(-) diff --git a/crates/uv-resolver/src/resolution/graph.rs b/crates/uv-resolver/src/resolution/graph.rs index 8098f1186d517..aa0dbd01bf16b 100644 --- a/crates/uv-resolver/src/resolution/graph.rs +++ b/crates/uv-resolver/src/resolution/graph.rs @@ -10,7 +10,7 @@ use distribution_types::{ }; use pep440_rs::{Version, VersionSpecifier}; use pep508_rs::{MarkerEnvironment, MarkerTree}; -use pypi_types::{ParsedUrlError, Requirement, Yanked}; +use pypi_types::{ParsedUrlError, Requirement, VerbatimParsedUrl, Yanked}; use uv_configuration::{Constraints, Overrides}; use uv_git::GitResolver; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -68,6 +68,7 @@ impl ResolutionGraph { type NodeKey<'a> = ( &'a PackageName, &'a Version, + Option<&'a VerbatimParsedUrl>, Option<&'a ExtraName>, Option<&'a GroupName>, ); @@ -245,7 +246,10 @@ impl ResolutionGraph { hashes, metadata, })); - inverse.insert((name, version, extra.as_ref(), dev.as_ref()), index); + inverse.insert( + (name, version, url.as_ref(), extra.as_ref(), dev.as_ref()), + index, + ); } } @@ -255,6 +259,7 @@ impl ResolutionGraph { inverse[&( from, &edge.from_version, + edge.from_url.as_ref(), edge.from_extra.as_ref(), edge.from_dev.as_ref(), )] @@ -262,6 +267,7 @@ impl ResolutionGraph { let to_index = inverse[&( &edge.to, &edge.to_version, + edge.to_url.as_ref(), edge.to_extra.as_ref(), edge.to_dev.as_ref(), )]; diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 52ce5d8a86de1..de33e65c68546 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -2227,6 +2227,7 @@ impl ForkState { _ => continue, }; + let self_url = self_name.as_ref().and_then(|name| self.fork_urls.get(name)); match **dependency_package { PubGrubPackageInner::Package { @@ -2238,13 +2239,16 @@ impl ForkState { if self_name.is_some_and(|self_name| self_name == dependency_name) { continue; } + let to_url = self.fork_urls.get(dependency_name); let edge = ResolutionDependencyEdge { from: self_name.cloned(), from_version: self_version.clone(), + from_url: self_url.cloned(), from_extra: self_extra.cloned(), from_dev: self_dev.cloned(), to: dependency_name.clone(), to_version: dependency_version.clone(), + to_url: to_url.cloned(), to_extra: dependency_extra.clone(), to_dev: dependency_dev.clone(), marker: None, @@ -2260,13 +2264,16 @@ impl ForkState { if self_name.is_some_and(|self_name| self_name == dependency_name) { continue; } + let to_url = self.fork_urls.get(dependency_name); let edge = ResolutionDependencyEdge { from: self_name.cloned(), from_version: self_version.clone(), + from_url: self_url.cloned(), from_extra: self_extra.cloned(), from_dev: self_dev.cloned(), to: dependency_name.clone(), to_version: dependency_version.clone(), + to_url: to_url.cloned(), to_extra: None, to_dev: None, marker: Some(dependency_marker.clone()), @@ -2283,13 +2290,16 @@ impl ForkState { if self_name.is_some_and(|self_name| self_name == dependency_name) { continue; } + let to_url = self.fork_urls.get(dependency_name); let edge = ResolutionDependencyEdge { from: self_name.cloned(), from_version: self_version.clone(), + from_url: self_url.cloned(), from_extra: self_extra.cloned(), from_dev: self_dev.cloned(), to: dependency_name.clone(), to_version: dependency_version.clone(), + to_url: to_url.cloned(), to_extra: Some(dependency_extra.clone()), to_dev: None, marker: dependency_marker.clone(), @@ -2306,13 +2316,16 @@ impl ForkState { if self_name.is_some_and(|self_name| self_name == dependency_name) { continue; } + let to_url = self.fork_urls.get(dependency_name); let edge = ResolutionDependencyEdge { from: self_name.cloned(), from_version: self_version.clone(), + from_url: self_url.cloned(), from_extra: self_extra.cloned(), from_dev: self_dev.cloned(), to: dependency_name.clone(), to_version: dependency_version.clone(), + to_url: to_url.cloned(), to_extra: None, to_dev: Some(dependency_dev.clone()), marker: dependency_marker.clone(), @@ -2390,10 +2403,12 @@ pub(crate) struct ResolutionDependencyEdge { /// This value is `None` if the dependency comes from the root package. pub(crate) from: Option, pub(crate) from_version: Version, + pub(crate) from_url: Option, pub(crate) from_extra: Option, pub(crate) from_dev: Option, pub(crate) to: PackageName, pub(crate) to_version: Version, + pub(crate) to_url: Option, pub(crate) to_extra: Option, pub(crate) to_dev: Option, pub(crate) marker: Option, diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 740ea34f2ac0a..2a8d23ee9ad40 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -4293,3 +4293,147 @@ fn lock_requires_python_no_wheels() -> Result<()> { Ok(()) } + +/// In this case, a package is included twice at the same version, but pointing to different direct +/// URLs. +#[test] +fn lock_same_version_multiple_urls() -> Result<()> { + let context = TestContext::new("3.12"); + + let v1 = context.temp_dir.child("v1"); + fs_err::create_dir_all(&v1)?; + let pyproject_toml = v1.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "dependency" + version = "0.0.1" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + "#, + )?; + + let v2 = context.temp_dir.child("v2"); + fs_err::create_dir_all(&v2)?; + let pyproject_toml = v2.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "dependency" + version = "0.0.1" + requires-python = ">=3.12" + dependencies = ["anyio==3.0.0"] + "#, + )?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(&formatdoc! { + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "dependency @ file://{}/v1 ; sys_platform == 'darwin'", + "dependency @ file://{}/v2 ; sys_platform != 'darwin'", + ] + "#, + context.temp_dir.display(), + context.temp_dir.display(), + })?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 7 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + exclude-newer = "2024-03-25 00:00:00 UTC" + + [[distribution]] + name = "anyio" + version = "3.0.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/99/0d/65165f99e5f4f3b4c43a5ed9db0fb7aa655f5a58f290727a30528a87eb45/anyio-3.0.0.tar.gz", hash = "sha256:b553598332c050af19f7d41f73a7790142f5bc3d5eb8bd82f5e515ec22019bd9", size = 116952 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/49/ebee263b69fe243bd1fd0a88bc6bb0f7732bf1794ba3273cb446351f9482/anyio-3.0.0-py3-none-any.whl", hash = "sha256:e71c3d9d72291d12056c0265d07c6bbedf92332f78573e278aeb116f24f30395", size = 72182 }, + ] + + [[distribution]] + name = "anyio" + version = "3.7.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, + ] + + [[distribution]] + name = "dependency" + version = "0.0.1" + source = { directory = "[TEMP_DIR]/v1" } + dependencies = [ + { name = "anyio", version = "3.7.0", source = { registry = "https://pypi.org/simple" } }, + ] + + [[distribution]] + name = "dependency" + version = "0.0.1" + source = { directory = "[TEMP_DIR]/v2" } + dependencies = [ + { name = "anyio", version = "3.0.0", source = { registry = "https://pypi.org/simple" } }, + ] + + [[distribution]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[distribution]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "dependency", version = "0.0.1", source = { directory = "[TEMP_DIR]/v1" }, marker = "sys_platform == 'darwin'" }, + { name = "dependency", version = "0.0.1", source = { directory = "[TEMP_DIR]/v2" }, marker = "sys_platform != 'darwin'" }, + ] + + [[distribution]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + Ok(()) +}