Skip to content

Commit

Permalink
Respect self-constraints on recursive extras
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Dec 8, 2024
1 parent 1b4bd8d commit 62f3b2f
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 43 deletions.
10 changes: 10 additions & 0 deletions crates/uv-pypi-types/src/requirement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,16 @@ impl RequirementSource {
matches!(self, Self::Directory { editable: true, .. })
}

/// Returns `true` if the source is empty.
pub fn is_empty(&self) -> bool {
match self {
Self::Registry { specifier, .. } => specifier.is_empty(),
Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
false
}
}
}

/// If the source is the registry, return the version specifiers
pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> {
match self {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/pubgrub/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1328,7 +1328,7 @@ impl std::fmt::Display for PubGrubHint {
Self::DependsOnItself { package } => {
write!(
f,
"{}{} The package `{}` depends on itself. This is likely a mistake. Consider removing the dependency.",
"{}{} The package `{}` depends on itself at an incompatible version. This is likely a mistake. Consider removing the dependency.",
"hint".bold().cyan(),
":".bold(),
package.cyan(),
Expand Down
23 changes: 21 additions & 2 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1567,7 +1567,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
return requirements;
}

// Check if there are recursive self inclusions and we need to go into the expensive branch.
// Check if there are recursive self inclusions; if so, we need to go into the expensive
// branch.
if !requirements
.iter()
.any(|req| name == Some(&req.name) && !req.extras.is_empty())
Expand Down Expand Up @@ -1624,8 +1625,26 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
}
}

// Retain any self-constraints for that extra, e.g., if `project[foo]` includes
// `project[bar]>1.0`, as a dependency, we need to propagate `project>1.0`, in addition to
// transitively expanding `project[bar]`.
let mut self_constraints = vec![];
for req in &requirements {
if name == Some(&req.name) && !req.source.is_empty() {
self_constraints.push(Requirement {
name: req.name.clone(),
extras: vec![],
groups: req.groups.clone(),
source: req.source.clone(),
origin: req.origin.clone(),
marker: req.marker,
});
}
}

// Drop all the self-requirements now that we flattened them out.
requirements.retain(|req| name != Some(&req.name));
requirements.retain(|req| name != Some(&req.name) || req.extras.is_empty());
requirements.extend(self_constraints.into_iter().map(Cow::Owned));

requirements
}
Expand Down
77 changes: 37 additions & 40 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19457,7 +19457,7 @@ fn lock_self_incompatible() -> Result<()> {
× No solution found when resolving dependencies:
╰─▶ Because your project depends on itself at an incompatible version (project==0.2.0), we can conclude that your project's requirements are unsatisfiable.

hint: The package `project` depends on itself. This is likely a mistake. Consider removing the dependency.
hint: The package `project` depends on itself at an incompatible version. This is likely a mistake. Consider removing the dependency.
"###);

Ok(())
Expand Down Expand Up @@ -19566,10 +19566,9 @@ fn lock_self_extra_to_extra_compatible() -> Result<()> {
}

#[test]
fn lock_self_extra_to_extra_incompatible() -> Result<()> {
fn lock_self_extra_to_same_extra_incompatible() -> Result<()> {
let context = TestContext::new("3.12");

// TODO(charlie): This should fail, but currently succeeds.
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
Expand All @@ -19585,52 +19584,50 @@ fn lock_self_extra_to_extra_incompatible() -> Result<()> {
)?;

uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
success: false
exit_code: 1
----- stdout -----

----- stderr -----
Resolved 2 packages in [TIME]
"###);
× No solution found when resolving dependencies:
╰─▶ Because project[foo] depends on your project and your project requires project[foo], we can conclude that your project's requirements are unsatisfiable.

let lock = context.read("uv.lock");
hint: The package `project[foo]` depends on itself at an incompatible version. This is likely a mistake. Consider removing the dependency.
"###);

insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.12"
Ok(())
}

[options]
exclude-newer = "2024-03-25T00:00:00Z"
#[test]
fn lock_self_extra_to_other_extra_incompatible() -> Result<()> {
let context = TestContext::new("3.12");

[[package]]
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "typing-extensions" },
]
requires-python = ">=3.12"
dependencies = ["typing-extensions"]

[package.metadata]
requires-dist = [
{ name = "project", extras = ["foo"], marker = "extra == 'foo'", specifier = "==0.2.0" },
{ name = "typing-extensions" },
]
[project.optional-dependencies]
foo = ["project[bar]==0.2.0"]
bar = ["iniconfig"]
"#,
)?;

[[package]]
name = "typing-extensions"
version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 },
]
"###
);
});
uv_snapshot!(context.filters(), context.lock(), @r###"
success: false
exit_code: 1
----- stdout -----

----- stderr -----
× No solution found when resolving dependencies:
╰─▶ Because project[foo] depends on your project and your project requires project[foo], we can conclude that your project's requirements are unsatisfiable.

hint: The package `project[foo]` depends on itself at an incompatible version. This is likely a mistake. Consider removing the dependency.
"###);

Ok(())
}
Expand Down Expand Up @@ -19764,7 +19761,7 @@ fn lock_self_extra_incompatible() -> Result<()> {
× No solution found when resolving dependencies:
╰─▶ Because project[foo] depends on your project and your project requires project[foo], we can conclude that your project's requirements are unsatisfiable.

hint: The package `project[foo]` depends on itself. This is likely a mistake. Consider removing the dependency.
hint: The package `project[foo]` depends on itself at an incompatible version. This is likely a mistake. Consider removing the dependency.
"###);

Ok(())
Expand Down Expand Up @@ -19893,7 +19890,7 @@ fn lock_self_marker_incompatible() -> Result<()> {
× No solution found when resolving dependencies:
╰─▶ Because only project{sys_platform == 'win32'}<=0.1 is available and your project depends on project{sys_platform == 'win32'}>0.1, we can conclude that your project's requirements are unsatisfiable.

hint: The package `project` depends on itself. This is likely a mistake. Consider removing the dependency.
hint: The package `project` depends on itself at an incompatible version. This is likely a mistake. Consider removing the dependency.
"###);

Ok(())
Expand Down

0 comments on commit 62f3b2f

Please sign in to comment.