mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Merge markers when applying constraints (#4648)
## Summary When a constraint is applied to a requirement with a marker, the marker needs to be propagated to the constraint. If both the constraint and the requirement have a marker, they need to be merged together (via `and`). Closes https://github.com/astral-sh/uv/issues/4575.
This commit is contained in:
parent
0bb99952f6
commit
ea6185e082
2 changed files with 196 additions and 122 deletions
|
@ -1218,7 +1218,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
dev: Option<&'a GroupName>,
|
||||
name: Option<&PackageName>,
|
||||
markers: &'a MarkerTree,
|
||||
) -> Vec<&'a Requirement> {
|
||||
) -> Vec<Cow<'a, Requirement>> {
|
||||
// Start with the requirements for the current extra of the package (for an extra
|
||||
// requirement) or the non-extra (regular) dependencies (if extra is None), plus
|
||||
// the constraints for the current package.
|
||||
|
@ -1227,9 +1227,9 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
} else {
|
||||
Either::Right(dependencies.iter())
|
||||
};
|
||||
let mut requirements: Vec<&Requirement> = self
|
||||
let mut requirements = self
|
||||
.requirements_for_extra(regular_and_dev_dependencies, extra, markers)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Check if there are recursive self inclusions and we need to go into the expensive branch.
|
||||
if !requirements
|
||||
|
@ -1245,21 +1245,23 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
let mut queue: VecDeque<_> = requirements
|
||||
.iter()
|
||||
.filter(|req| name == Some(&req.name))
|
||||
.flat_map(|req| &req.extras)
|
||||
.flat_map(|req| req.extras.iter().cloned())
|
||||
.collect();
|
||||
while let Some(extra) = queue.pop_front() {
|
||||
if !seen.insert(extra) {
|
||||
if !seen.insert(extra.clone()) {
|
||||
continue;
|
||||
}
|
||||
// Add each transitively included extra.
|
||||
queue.extend(
|
||||
self.requirements_for_extra(dependencies, Some(extra), markers)
|
||||
.filter(|req| name == Some(&req.name))
|
||||
.flat_map(|req| &req.extras),
|
||||
);
|
||||
// Add the requirements for that extra
|
||||
requirements.extend(self.requirements_for_extra(dependencies, Some(extra), markers));
|
||||
for requirement in self.requirements_for_extra(dependencies, Some(&extra), markers) {
|
||||
if name == Some(&requirement.name) {
|
||||
// Add each transitively included extra.
|
||||
queue.extend(requirement.extras.iter().cloned());
|
||||
} else {
|
||||
// Add the requirements for that extra.
|
||||
requirements.push(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drop all the self-requirements now that we flattened them out.
|
||||
requirements.retain(|req| name != Some(&req.name));
|
||||
|
||||
|
@ -1268,12 +1270,15 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
|
||||
/// The set of the regular and dev dependencies, filtered by Python version,
|
||||
/// the markers of this fork and the requested extra.
|
||||
fn requirements_for_extra<'a>(
|
||||
&'a self,
|
||||
dependencies: impl IntoIterator<Item = &'a Requirement>,
|
||||
extra: Option<&'a ExtraName>,
|
||||
markers: &'a MarkerTree,
|
||||
) -> impl Iterator<Item = &'a Requirement> {
|
||||
fn requirements_for_extra<'data, 'parameters>(
|
||||
&'data self,
|
||||
dependencies: impl IntoIterator<Item = &'data Requirement> + 'parameters,
|
||||
extra: Option<&'parameters ExtraName>,
|
||||
markers: &'parameters MarkerTree,
|
||||
) -> impl Iterator<Item = Cow<'data, Requirement>> + 'parameters
|
||||
where
|
||||
'data: 'parameters,
|
||||
{
|
||||
self.overrides
|
||||
.apply(dependencies)
|
||||
.filter(move |requirement| {
|
||||
|
@ -1322,7 +1327,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
true
|
||||
})
|
||||
.flat_map(move |requirement| {
|
||||
iter::once(requirement).chain(
|
||||
iter::once(Cow::Borrowed(requirement)).chain(
|
||||
self.constraints
|
||||
.get(&requirement.name)
|
||||
.into_iter()
|
||||
|
@ -1359,7 +1364,27 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
}
|
||||
|
||||
true
|
||||
}),
|
||||
})
|
||||
.map(move |constraint| {
|
||||
// If the requirement is `requests ; sys_platform == 'darwin'` and the
|
||||
// constraint is `requests ; python_version == '3.6'`, the constraint
|
||||
// should only apply when _both_ markers are true.
|
||||
if let Some(marker) = requirement.marker.as_ref() {
|
||||
let marker = constraint.marker.as_ref().map(|m| {
|
||||
MarkerTree::And(vec![marker.clone(), m.clone()])
|
||||
}).or_else(|| Some(marker.clone()));
|
||||
|
||||
Cow::Owned(Requirement {
|
||||
name: constraint.name.clone(),
|
||||
extras: constraint.extras.clone(),
|
||||
source: constraint.source.clone(),
|
||||
origin: constraint.origin.clone(),
|
||||
marker
|
||||
})
|
||||
} else {
|
||||
Cow::Borrowed(constraint)
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6390,7 +6390,8 @@ fn universal_cycles() -> Result<()> {
|
|||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str(indoc::indoc! {r"
|
||||
poetry
|
||||
testtools==2.3.0
|
||||
fixtures==3.0.0
|
||||
"})?;
|
||||
|
||||
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
|
||||
|
@ -6401,114 +6402,162 @@ fn universal_cycles() -> Result<()> {
|
|||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
|
||||
build==1.1.1
|
||||
# via poetry
|
||||
cachecontrol==0.14.0
|
||||
# via poetry
|
||||
certifi==2024.2.2
|
||||
# via requests
|
||||
cffi==1.16.0 ; sys_platform == 'darwin' or (platform_python_implementation != 'PyPy' and sys_platform == 'linux')
|
||||
# via
|
||||
# cryptography
|
||||
# xattr
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
cleo==2.1.0
|
||||
# via poetry
|
||||
colorama==0.4.6 ; os_name == 'nt'
|
||||
# via build
|
||||
crashtest==0.4.1
|
||||
# via
|
||||
# cleo
|
||||
# poetry
|
||||
cryptography==42.0.5 ; sys_platform == 'linux'
|
||||
# via secretstorage
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
dulwich==0.21.7
|
||||
# via poetry
|
||||
fastjsonschema==2.19.1
|
||||
# via poetry
|
||||
filelock==3.13.1
|
||||
# via
|
||||
# cachecontrol
|
||||
# virtualenv
|
||||
idna==3.6
|
||||
# via requests
|
||||
installer==0.7.0
|
||||
# via poetry
|
||||
jaraco-classes==3.3.1
|
||||
# via keyring
|
||||
jeepney==0.8.0 ; sys_platform == 'linux'
|
||||
# via
|
||||
# keyring
|
||||
# secretstorage
|
||||
keyring==24.3.1
|
||||
# via poetry
|
||||
more-itertools==10.2.0
|
||||
# via jaraco-classes
|
||||
msgpack==1.0.8
|
||||
# via cachecontrol
|
||||
packaging==24.0
|
||||
# via
|
||||
# build
|
||||
# poetry
|
||||
pexpect==4.9.0
|
||||
# via poetry
|
||||
pkginfo==1.10.0
|
||||
# via poetry
|
||||
platformdirs==4.2.0
|
||||
# via
|
||||
# poetry
|
||||
# virtualenv
|
||||
poetry==1.8.2
|
||||
argparse==1.4.0
|
||||
# via unittest2
|
||||
extras==1.0.0
|
||||
# via testtools
|
||||
fixtures==3.0.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# poetry-plugin-export
|
||||
poetry-core==1.9.0
|
||||
# testtools
|
||||
linecache2==1.0.0
|
||||
# via traceback2
|
||||
pbr==6.0.0
|
||||
# via
|
||||
# poetry
|
||||
# poetry-plugin-export
|
||||
poetry-plugin-export==1.7.1
|
||||
# via poetry
|
||||
ptyprocess==0.7.0
|
||||
# via pexpect
|
||||
pycparser==2.21 ; sys_platform == 'darwin' or (platform_python_implementation != 'PyPy' and sys_platform == 'linux')
|
||||
# via cffi
|
||||
pyproject-hooks==1.0.0
|
||||
# fixtures
|
||||
# testtools
|
||||
python-mimeparse==1.6.0
|
||||
# via testtools
|
||||
six==1.16.0
|
||||
# via
|
||||
# build
|
||||
# poetry
|
||||
pywin32-ctypes==0.2.2 ; sys_platform == 'win32'
|
||||
# via keyring
|
||||
rapidfuzz==3.7.0
|
||||
# via cleo
|
||||
requests==2.31.0
|
||||
# fixtures
|
||||
# testtools
|
||||
# unittest2
|
||||
testtools==2.3.0
|
||||
# via
|
||||
# cachecontrol
|
||||
# poetry
|
||||
# requests-toolbelt
|
||||
requests-toolbelt==1.0.0
|
||||
# via poetry
|
||||
secretstorage==3.3.3 ; sys_platform == 'linux'
|
||||
# via keyring
|
||||
shellingham==1.5.4
|
||||
# via poetry
|
||||
tomlkit==0.12.4
|
||||
# via poetry
|
||||
trove-classifiers==2024.3.3
|
||||
# via poetry
|
||||
urllib3==2.2.1
|
||||
# -r requirements.in
|
||||
# fixtures
|
||||
traceback2==1.4.0
|
||||
# via
|
||||
# dulwich
|
||||
# requests
|
||||
virtualenv==20.25.1
|
||||
# via poetry
|
||||
xattr==1.1.0 ; sys_platform == 'darwin'
|
||||
# via poetry
|
||||
# testtools
|
||||
# unittest2
|
||||
unittest2==1.1.0
|
||||
# via testtools
|
||||
|
||||
----- stderr -----
|
||||
Resolved 41 packages in [TIME]
|
||||
Resolved 10 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform a universal resolution with a constraint.
|
||||
#[test]
|
||||
fn universal_constraint() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str(indoc::indoc! {r"
|
||||
anyio ; sys_platform == 'win32'
|
||||
"})?;
|
||||
|
||||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||||
constraints_txt.write_str(indoc::indoc! {r"
|
||||
anyio==3.0.0
|
||||
"})?;
|
||||
|
||||
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
|
||||
.arg("requirements.in")
|
||||
.arg("-c")
|
||||
.arg("constraints.txt")
|
||||
.arg("--universal"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in -c constraints.txt --universal
|
||||
anyio==3.0.0 ; sys_platform == 'win32'
|
||||
# via
|
||||
# -c constraints.txt
|
||||
# -r requirements.in
|
||||
idna==3.6 ; sys_platform == 'win32'
|
||||
# via anyio
|
||||
sniffio==1.3.1 ; sys_platform == 'win32'
|
||||
# via anyio
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform a universal resolution with a constraint, where the constraint itself has a marker.
|
||||
#[test]
|
||||
fn universal_constraint_marker() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str(indoc::indoc! {r"
|
||||
anyio ; sys_platform == 'win32'
|
||||
"})?;
|
||||
|
||||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||||
constraints_txt.write_str(indoc::indoc! {r"
|
||||
anyio==3.0.0 ; os_name == 'nt'
|
||||
"})?;
|
||||
|
||||
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
|
||||
.arg("requirements.in")
|
||||
.arg("-c")
|
||||
.arg("constraints.txt")
|
||||
.arg("--universal"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] requirements.in -c constraints.txt --universal
|
||||
anyio==3.0.0 ; sys_platform == 'win32' or (os_name == 'nt' and sys_platform == 'win32')
|
||||
# via
|
||||
# -c constraints.txt
|
||||
# -r requirements.in
|
||||
idna==3.6 ; sys_platform == 'win32' or (os_name == 'nt' and sys_platform == 'win32')
|
||||
# via anyio
|
||||
sniffio==1.3.1 ; sys_platform == 'win32' or (os_name == 'nt' and sys_platform == 'win32')
|
||||
# via anyio
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform a universal resolution with a divergent requirement, and a third requirement that's
|
||||
/// compatible with both forks.
|
||||
///
|
||||
/// This currently fails, but should succeed.
|
||||
///
|
||||
/// See: <https://github.com/astral-sh/uv/issues/4640>
|
||||
#[test]
|
||||
fn universal_multi_version() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str(indoc::indoc! {r"
|
||||
iniconfig
|
||||
iniconfig==2.0.0 ; python_version > '3.12'
|
||||
iniconfig==1.0.0 ; python_version == '3.12'
|
||||
"})?;
|
||||
|
||||
let constraints_txt = context.temp_dir.child("constraints.txt");
|
||||
constraints_txt.write_str(indoc::indoc! {r"
|
||||
anyio==3.0.0 ; os_name == 'nt'
|
||||
"})?;
|
||||
|
||||
uv_snapshot!(context.filters(), windows_filters=false, context.pip_compile()
|
||||
.arg("requirements.in")
|
||||
.arg("-c")
|
||||
.arg("constraints.txt")
|
||||
.arg("--universal"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because iniconfig{python_version > '3.12'}==2.0.0 depends on iniconfig==2.0.0 and you require iniconfig{python_version > '3.12'}==2.0.0, we can conclude that your requirements and iniconfig{python_version == '3.12'}==1.0.0 are incompatible.
|
||||
And because you require iniconfig{python_version == '3.12'}==1.0.0, we can conclude that the requirements are unsatisfiable.
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue