Skip to content

Commit 287d2f3

Browse files
committed
Respect self-constraints on recursive extras
1 parent 1b4bd8d commit 287d2f3

File tree

4 files changed

+73
-43
lines changed

4 files changed

+73
-43
lines changed

crates/uv-pypi-types/src/requirement.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,16 @@ impl RequirementSource {
562562
matches!(self, Self::Directory { editable: true, .. })
563563
}
564564

565+
/// Returns `true` if the source is empty.
566+
pub fn is_empty(&self) -> bool {
567+
match self {
568+
Self::Registry { specifier, .. } => specifier.is_empty(),
569+
Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
570+
false
571+
}
572+
}
573+
}
574+
565575
/// If the source is the registry, return the version specifiers
566576
pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> {
567577
match self {

crates/uv-resolver/src/pubgrub/report.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1328,7 +1328,7 @@ impl std::fmt::Display for PubGrubHint {
13281328
Self::DependsOnItself { package } => {
13291329
write!(
13301330
f,
1331-
"{}{} The package `{}` depends on itself. This is likely a mistake. Consider removing the dependency.",
1331+
"{}{} The package `{}` depends on itself at an incompatible version. This is likely a mistake. Consider removing the dependency.",
13321332
"hint".bold().cyan(),
13331333
":".bold(),
13341334
package.cyan(),

crates/uv-resolver/src/resolver/mod.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,7 +1567,8 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
15671567
return requirements;
15681568
}
15691569

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

1628+
// Retain any self-constraints for that extra, e.g., if `project[foo]` includes
1629+
// `project[bar]>1.0`, as a dependency, we need to propagate `project>1.0`, in addition to
1630+
// transitively expanding `project[bar]`.
1631+
let mut self_constraints = vec![];
1632+
for req in &requirements {
1633+
if name == Some(&req.name) && !req.source.is_empty() {
1634+
self_constraints.push(Requirement {
1635+
name: req.name.clone(),
1636+
extras: vec![],
1637+
groups: req.groups.clone(),
1638+
source: req.source.clone(),
1639+
origin: req.origin.clone(),
1640+
marker: req.marker,
1641+
});
1642+
}
1643+
}
1644+
16271645
// Drop all the self-requirements now that we flattened them out.
1628-
requirements.retain(|req| name != Some(&req.name));
1646+
requirements.retain(|req| name != Some(&req.name) || req.extras.is_empty());
1647+
requirements.extend(self_constraints.into_iter().map(Cow::Owned));
16291648

16301649
requirements
16311650
}

crates/uv/tests/it/lock.rs

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19131,6 +19131,10 @@ fn lock_recursive_extra() -> Result<()> {
1913119131
lock, @r###"
1913219132
version = 1
1913319133
requires-python = ">=3.12"
19134+
resolution-markers = [
19135+
"python_full_version >= '3.13' or sys_platform != 'darwin'",
19136+
"python_full_version < '3.13' and sys_platform == 'darwin'",
19137+
]
1913419138

1913519139
[options]
1913619140
exclude-newer = "2024-03-25T00:00:00Z"
@@ -19457,7 +19461,7 @@ fn lock_self_incompatible() -> Result<()> {
1945719461
× No solution found when resolving dependencies:
1945819462
╰─▶ Because your project depends on itself at an incompatible version (project==0.2.0), we can conclude that your project's requirements are unsatisfiable.
1945919463

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

1946319467
Ok(())
@@ -19566,10 +19570,9 @@ fn lock_self_extra_to_extra_compatible() -> Result<()> {
1956619570
}
1956719571

1956819572
#[test]
19569-
fn lock_self_extra_to_extra_incompatible() -> Result<()> {
19573+
fn lock_self_extra_to_same_extra_incompatible() -> Result<()> {
1957019574
let context = TestContext::new("3.12");
1957119575

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

1958719590
uv_snapshot!(context.filters(), context.lock(), @r###"
19588-
success: true
19589-
exit_code: 0
19591+
success: false
19592+
exit_code: 1
1959019593
----- stdout -----
1959119594

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

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

19598-
insta::with_settings!({
19599-
filters => context.filters(),
19600-
}, {
19601-
assert_snapshot!(
19602-
lock, @r###"
19603-
version = 1
19604-
requires-python = ">=3.12"
19602+
Ok(())
19603+
}
1960519604

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

19609-
[[package]]
19609+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
19610+
pyproject_toml.write_str(
19611+
r#"
19612+
[project]
1961019613
name = "project"
1961119614
version = "0.1.0"
19612-
source = { virtual = "." }
19613-
dependencies = [
19614-
{ name = "typing-extensions" },
19615-
]
19615+
requires-python = ">=3.12"
19616+
dependencies = ["typing-extensions"]
1961619617

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

19623-
[[package]]
19624-
name = "typing-extensions"
19625-
version = "4.10.0"
19626-
source = { registry = "https://pypi.org/simple" }
19627-
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 }
19628-
wheels = [
19629-
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 },
19630-
]
19631-
"###
19632-
);
19633-
});
19624+
uv_snapshot!(context.filters(), context.lock(), @r###"
19625+
success: false
19626+
exit_code: 1
19627+
----- stdout -----
19628+
19629+
----- stderr -----
19630+
× No solution found when resolving dependencies:
19631+
╰─▶ Because project[foo] depends on your project and your project requires project[foo], we can conclude that your project's requirements are unsatisfiable.
19632+
19633+
hint: The package `project[foo]` depends on itself at an incompatible version. This is likely a mistake. Consider removing the dependency.
19634+
"###);
1963419635

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

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

1977019771
Ok(())
@@ -19893,7 +19894,7 @@ fn lock_self_marker_incompatible() -> Result<()> {
1989319894
× No solution found when resolving dependencies:
1989419895
╰─▶ 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.
1989519896

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

1989919900
Ok(())

0 commit comments

Comments
 (0)