Skip to content

Commit

Permalink
Propagate markers for recursive extras in resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Nov 28, 2024
1 parent 4a07414 commit 2bc6ded
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 5 deletions.
44 changes: 39 additions & 5 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1262,9 +1262,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
let dependencies = match &**package {
PubGrubPackageInner::Root(_) => {
let no_dev_deps = BTreeMap::default();
let no_provides_extras = [];
let requirements = self.flatten_requirements(
&self.requirements,
&no_dev_deps,
&no_provides_extras,
None,
None,
None,
Expand Down Expand Up @@ -1452,6 +1454,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
let requirements = self.flatten_requirements(
&metadata.requires_dist,
&metadata.dependency_groups,
&metadata.provides_extras,
extra.as_ref(),
dev.as_ref(),
Some(name),
Expand Down Expand Up @@ -1576,6 +1579,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&'a self,
dependencies: &'a [Requirement],
dev_dependencies: &'a BTreeMap<GroupName, Vec<Requirement>>,
extras: &'a [ExtraName],
extra: Option<&'a ExtraName>,
dev: Option<&'a GroupName>,
name: Option<&PackageName>,
Expand Down Expand Up @@ -1610,22 +1614,52 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag

// Transitively process all extras that are recursively included, starting with the current
// extra.
let mut seen = FxHashSet::default();
let mut seen = FxHashSet::<(ExtraName, MarkerTree)>::default();
let mut queue: VecDeque<_> = requirements
.iter()
.filter(|req| name == Some(&req.name))
.flat_map(|req| req.extras.iter().cloned())
.flat_map(|req| {
req.extras
.iter()
.cloned()
.map(|extra| (extra, req.marker.clone().simplify_extras(extras)))
})
.collect();
while let Some(extra) = queue.pop_front() {
if !seen.insert(extra.clone()) {
while let Some((extra, marker)) = queue.pop_front() {
if !seen.insert((extra.clone(), marker.clone())) {
continue;
}
for requirement in
self.requirements_for_extra(dependencies, Some(&extra), env, python_requirement)
{
let requirement = if marker.is_true() {
requirement
} else {
match requirement {
Cow::Owned(mut requirement) => {
requirement.marker.and(marker.clone());
Cow::Owned(requirement)
}
Cow::Borrowed(requirement) => {
let mut marker = marker.clone();
marker.and(requirement.marker.clone());
Cow::Owned(Requirement {
name: requirement.name.clone(),
extras: requirement.extras.clone(),
source: requirement.source.clone(),
origin: requirement.origin.clone(),
marker,
})
}
}
};
if name == Some(&requirement.name) {
// Add each transitively included extra.
queue.extend(requirement.extras.iter().cloned());
queue.extend(
requirement.extras.iter().cloned().map(|extra| {
(extra, requirement.marker.clone().simplify_extras(extras))
}),
);
} else {
// Add the requirements for that extra.
requirements.push(requirement);
Expand Down
100 changes: 100 additions & 0 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18910,3 +18910,103 @@ fn mismatched_name_self_editable() -> Result<()> {

Ok(())
}

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

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []

[project.optional-dependencies]
foo = ["iniconfig"]
bar = ["project[foo]"]
baz = ["project[bar]"]
bop = ["project[bar] ; sys_platform == 'darwin'"]
qux = ["project[bop] ; python_version == '3.12'"]
"#,
)?;

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

----- stderr -----
Resolved 2 packages in [TIME]
"###);

let lock = context.read("uv.lock");

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

[options]
exclude-newer = "2024-03-25T00:00:00Z"

[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]

[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }

[package.optional-dependencies]
bar = [
{ name = "iniconfig" },
]
baz = [
{ name = "iniconfig" },
]
bop = [
{ name = "iniconfig", marker = "sys_platform == 'darwin'" },
]
foo = [
{ name = "iniconfig" },
]
qux = [
{ name = "iniconfig", marker = "python_full_version < '3.13' and sys_platform == 'darwin'" },
]

[package.metadata]
requires-dist = [
{ name = "iniconfig", marker = "extra == 'foo'" },
{ name = "project", extras = ["bar"], marker = "sys_platform == 'darwin' and extra == 'bop'" },
{ name = "project", extras = ["bar"], marker = "extra == 'baz'" },
{ name = "project", extras = ["bop"], marker = "python_full_version == '3.12.*' and extra == 'qux'" },
{ name = "project", extras = ["foo"], marker = "extra == 'bar'" },
]
"###
);
});

// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 2 packages in [TIME]
"###);

Ok(())
}

0 comments on commit 2bc6ded

Please sign in to comment.