Skip to content

Commit

Permalink
Include URLs on graph edges (#5312)
Browse files Browse the repository at this point in the history
## Summary

Excellent find from @konstin. If we have a package that's included in
two forks at the same version, but with different URLs, we need to avoid
collapsing them in the lockfile.

Closes #5294.
  • Loading branch information
charliermarsh authored Jul 22, 2024
1 parent 4f9ac3b commit 26e042a
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 2 deletions.
10 changes: 8 additions & 2 deletions crates/uv-resolver/src/resolution/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -68,6 +68,7 @@ impl ResolutionGraph {
type NodeKey<'a> = (
&'a PackageName,
&'a Version,
Option<&'a VerbatimParsedUrl>,
Option<&'a ExtraName>,
Option<&'a GroupName>,
);
Expand Down Expand Up @@ -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,
);
}
}

Expand All @@ -255,13 +259,15 @@ impl ResolutionGraph {
inverse[&(
from,
&edge.from_version,
edge.from_url.as_ref(),
edge.from_extra.as_ref(),
edge.from_dev.as_ref(),
)]
});
let to_index = inverse[&(
&edge.to,
&edge.to_version,
edge.to_url.as_ref(),
edge.to_extra.as_ref(),
edge.to_dev.as_ref(),
)];
Expand Down
15 changes: 15 additions & 0 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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()),
Expand All @@ -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(),
Expand All @@ -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(),
Expand Down Expand Up @@ -2390,10 +2403,12 @@ pub(crate) struct ResolutionDependencyEdge {
/// This value is `None` if the dependency comes from the root package.
pub(crate) from: Option<PackageName>,
pub(crate) from_version: Version,
pub(crate) from_url: Option<VerbatimParsedUrl>,
pub(crate) from_extra: Option<ExtraName>,
pub(crate) from_dev: Option<GroupName>,
pub(crate) to: PackageName,
pub(crate) to_version: Version,
pub(crate) to_url: Option<VerbatimParsedUrl>,
pub(crate) to_extra: Option<ExtraName>,
pub(crate) to_dev: Option<GroupName>,
pub(crate) marker: Option<MarkerTree>,
Expand Down
145 changes: 145 additions & 0 deletions crates/uv/tests/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::Result;
use assert_fs::prelude::*;
use indoc::{formatdoc, indoc};
use insta::assert_snapshot;
use url::Url;

use common::{uv_snapshot, TestContext};

Expand Down Expand Up @@ -4293,3 +4294,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 @ {} ; sys_platform == 'darwin'",
"dependency @ {} ; sys_platform != 'darwin'",
]
"#,
Url::from_file_path(context.temp_dir.join("v1")).unwrap(),
Url::from_file_path(context.temp_dir.join("v2")).unwrap(),
})?;

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(())
}

0 comments on commit 26e042a

Please sign in to comment.