mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Fix handling of changes to requires-python
(#14076)
When using `uv lock --upgrade-package=python` after changing `requires-python`, it was possible to get into a state where the fork markers produced corresponded to the empty set. This in turn resulted in an empty lock file. There was already some infrastructure in place that I think was perhaps intended to handle this. In particular, `Lock::check_marker_coverage` checks whether the fork markers have some overlap with the supported environments (including the `requires-python`). But there were two problems with this. First is that in lock validation, this marker coverage check came _after_ a path that returned `Preferable` (meaning that the fork markers should be kept) when `--upgrade-package` was used. Second is that the marker coverage check used the `requires-python` in the lock file and _not_ the `requires-python` in the now updated `pyproject.toml`. We attempt to solve this conundrum by slightly re-arranging lock file validation and by explicitly checking whether the *new* `requires-python` is disjoint from the fork markers in the lock file. If it is, then we return `Versions` from lock file validation (indicating that the fork markers should be dropped). Fixes #13951
This commit is contained in:
parent
d653fbb133
commit
3d4f0c934e
4 changed files with 250 additions and 23 deletions
|
@ -768,6 +768,36 @@ impl Lock {
|
|||
}
|
||||
}
|
||||
|
||||
/// Checks whether the new requires-python specification is disjoint with
|
||||
/// the fork markers in this lock file.
|
||||
///
|
||||
/// If they are disjoint, then the union of the fork markers along with the
|
||||
/// given requires-python specification (converted to a marker tree) are
|
||||
/// returned.
|
||||
///
|
||||
/// When disjoint, the fork markers in the lock file should be dropped and
|
||||
/// not used.
|
||||
pub fn requires_python_coverage(
|
||||
&self,
|
||||
new_requires_python: &RequiresPython,
|
||||
) -> Result<(), (MarkerTree, MarkerTree)> {
|
||||
let fork_markers_union = if self.fork_markers().is_empty() {
|
||||
self.requires_python.to_marker_tree()
|
||||
} else {
|
||||
let mut fork_markers_union = MarkerTree::FALSE;
|
||||
for fork_marker in self.fork_markers() {
|
||||
fork_markers_union.or(fork_marker.pep508());
|
||||
}
|
||||
fork_markers_union
|
||||
};
|
||||
let new_requires_python = new_requires_python.to_marker_tree();
|
||||
if fork_markers_union.is_disjoint(new_requires_python) {
|
||||
Err((fork_markers_union, new_requires_python))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the TOML representation of this lockfile.
|
||||
pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
|
||||
// Catch a lockfile where the union of fork markers doesn't cover the supported
|
||||
|
|
|
@ -983,13 +983,54 @@ impl ValidatedLock {
|
|||
return Ok(Self::Unusable(lock));
|
||||
}
|
||||
Upgrade::Packages(_) => {
|
||||
// If the user specified `--upgrade-package`, then at best we can prefer some of
|
||||
// the existing versions.
|
||||
debug!("Ignoring existing lockfile due to `--upgrade-package`");
|
||||
return Ok(Self::Preferable(lock));
|
||||
// This is handled below, after some checks regarding fork
|
||||
// markers. In particular, we'd like to return `Preferable`
|
||||
// here, but we shouldn't if the fork markers cannot be
|
||||
// reused.
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: It's important that this appears before any possible path that
|
||||
// returns `Self::Preferable`. In particular, if our fork markers are
|
||||
// bunk, then we shouldn't return a result that indicates we should try
|
||||
// to re-use the existing fork markers.
|
||||
if let Err((fork_markers_union, environments_union)) = lock.check_marker_coverage() {
|
||||
warn_user!(
|
||||
"Ignoring existing lockfile due to fork markers not covering the supported environments: `{}` vs `{}`",
|
||||
fork_markers_union
|
||||
.try_to_string()
|
||||
.unwrap_or("true".to_string()),
|
||||
environments_union
|
||||
.try_to_string()
|
||||
.unwrap_or("true".to_string()),
|
||||
);
|
||||
return Ok(Self::Versions(lock));
|
||||
}
|
||||
|
||||
// NOTE: Similarly as above, this should also appear before any
|
||||
// possible code path that can return `Self::Preferable`.
|
||||
if let Err((fork_markers_union, requires_python_marker)) =
|
||||
lock.requires_python_coverage(requires_python)
|
||||
{
|
||||
warn_user!(
|
||||
"Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `{}` vs `{}`",
|
||||
fork_markers_union
|
||||
.try_to_string()
|
||||
.unwrap_or("true".to_string()),
|
||||
requires_python_marker
|
||||
.try_to_string()
|
||||
.unwrap_or("true".to_string()),
|
||||
);
|
||||
return Ok(Self::Versions(lock));
|
||||
}
|
||||
|
||||
if let Upgrade::Packages(_) = upgrade {
|
||||
// If the user specified `--upgrade-package`, then at best we can prefer some of
|
||||
// the existing versions.
|
||||
debug!("Ignoring existing lockfile due to `--upgrade-package`");
|
||||
return Ok(Self::Preferable(lock));
|
||||
}
|
||||
|
||||
// If the Requires-Python bound has changed, we have to perform a clean resolution, since
|
||||
// the set of `resolution-markers` may no longer cover the entire supported Python range.
|
||||
if lock.requires_python().range() != requires_python.range() {
|
||||
|
@ -1022,19 +1063,6 @@ impl ValidatedLock {
|
|||
return Ok(Self::Versions(lock));
|
||||
}
|
||||
|
||||
if let Err((fork_markers_union, environments_union)) = lock.check_marker_coverage() {
|
||||
warn_user!(
|
||||
"Ignoring existing lockfile due to fork markers not covering the supported environments: `{}` vs `{}`",
|
||||
fork_markers_union
|
||||
.try_to_string()
|
||||
.unwrap_or("true".to_string()),
|
||||
environments_union
|
||||
.try_to_string()
|
||||
.unwrap_or("true".to_string()),
|
||||
);
|
||||
return Ok(Self::Versions(lock));
|
||||
}
|
||||
|
||||
// If the set of required platforms has changed, we have to perform a clean resolution.
|
||||
let expected = lock.simplified_required_environments();
|
||||
let actual = required_environments
|
||||
|
|
|
@ -4731,15 +4731,16 @@ fn lock_requires_python_wheels() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.11.[X] interpreter at: [PYTHON-3.11]
|
||||
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.12.*'` vs `python_full_version == '3.11.*'`
|
||||
Resolved 2 packages in [TIME]
|
||||
"###);
|
||||
");
|
||||
|
||||
let lock = fs_err::read_to_string(&lockfile).unwrap();
|
||||
|
||||
|
@ -28020,6 +28021,170 @@ fn lock_conflict_for_disjoint_python_version() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that we hint if the resolution failed for a different platform.
|
||||
#[cfg(feature = "python-patch")]
|
||||
#[test]
|
||||
fn lock_requires_python_empty_lock_file() -> Result<()> {
|
||||
// N.B. These versions were selected based on what was
|
||||
// in `.python-versions` at the time of writing (2025-06-16).
|
||||
let (v1, v2) = ("3.13.0", "3.13.2");
|
||||
let context = TestContext::new_with_versions(&[v1, v2]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(&format!(
|
||||
r#"
|
||||
[project]
|
||||
name = "renovate-bug-repro"
|
||||
version = "0.1.0"
|
||||
requires-python = "=={v1}"
|
||||
dependencies = ["opencv-python-headless>=4.8"]
|
||||
"#,
|
||||
))?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.13.0 interpreter at: [PYTHON-3.13.0]
|
||||
Resolved 3 packages in [TIME]
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = "==3.13.0"
|
||||
resolution-markers = [
|
||||
"sys_platform == 'darwin'",
|
||||
"platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "1.26.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
|
||||
|
||||
[[package]]
|
||||
name = "opencv-python-headless"
|
||||
version = "4.9.0.80"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/b2/c308bc696bf5d75304175c62222ec8af9a6d5cfe36c14f19f15ea9d1a132/opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958", size = 92910044, upload-time = "2023-12-31T13:34:50.518Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/42/da433fca5733a3ce7e88dd0d4018f70dcffaf48770b5142250815f4faddb/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a", size = 55689478, upload-time = "2023-12-31T14:31:30.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/0c/a59f2a40d6058ee8126668dc5dff6977c913f6ecd21dbd15b41563409a18/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6", size = 35354670, upload-time = "2023-12-31T16:38:31.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/37/225a1f8be42610ffecf677558311ab0f9dfdc63537c250a2bce76762a380/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d", size = 28954368, upload-time = "2023-12-31T16:40:00.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/19/3c65483a80a1d062d46ae20faf5404712d25cb1dfdcaf371efbd67c38544/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df", size = 49591873, upload-time = "2023-12-31T13:34:44.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/98/300382ff6ddff3a487e808c8a76362e430f5016002fcbefb3b3117aad32b/opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670", size = 28488841, upload-time = "2023-12-31T13:34:31.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/44/458a0a135866f5e08266566b32ad9a182a7a059a894effe6c41a9c841ff1/opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c", size = 38536073, upload-time = "2023-12-31T13:34:39.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "renovate-bug-repro"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "opencv-python-headless" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "opencv-python-headless", specifier = ">=4.8" }]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
pyproject_toml.write_str(&format!(
|
||||
r#"
|
||||
[project]
|
||||
name = "renovate-bug-repro"
|
||||
version = "0.1.0"
|
||||
requires-python = "=={v2}"
|
||||
dependencies = ["opencv-python-headless>=4.8"]
|
||||
"#,
|
||||
))?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock().arg("--upgrade-package=python"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using CPython 3.13.2 interpreter at: [PYTHON-3.13.2]
|
||||
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version == '3.13.0'` vs `python_full_version == '3.13.2'`
|
||||
Resolved 3 packages in [TIME]
|
||||
");
|
||||
|
||||
let lock = context.read("uv.lock");
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r#"
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = "==3.13.2"
|
||||
resolution-markers = [
|
||||
"sys_platform == 'darwin'",
|
||||
"platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "1.26.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
|
||||
|
||||
[[package]]
|
||||
name = "opencv-python-headless"
|
||||
version = "4.9.0.80"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/b2/c308bc696bf5d75304175c62222ec8af9a6d5cfe36c14f19f15ea9d1a132/opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958", size = 92910044, upload-time = "2023-12-31T13:34:50.518Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/42/da433fca5733a3ce7e88dd0d4018f70dcffaf48770b5142250815f4faddb/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a", size = 55689478, upload-time = "2023-12-31T14:31:30.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/0c/a59f2a40d6058ee8126668dc5dff6977c913f6ecd21dbd15b41563409a18/opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6", size = 35354670, upload-time = "2023-12-31T16:38:31.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/37/225a1f8be42610ffecf677558311ab0f9dfdc63537c250a2bce76762a380/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d", size = 28954368, upload-time = "2023-12-31T16:40:00.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/19/3c65483a80a1d062d46ae20faf5404712d25cb1dfdcaf371efbd67c38544/opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df", size = 49591873, upload-time = "2023-12-31T13:34:44.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/98/300382ff6ddff3a487e808c8a76362e430f5016002fcbefb3b3117aad32b/opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670", size = 28488841, upload-time = "2023-12-31T13:34:31.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/44/458a0a135866f5e08266566b32ad9a182a7a059a894effe6c41a9c841ff1/opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c", size = 38536073, upload-time = "2023-12-31T13:34:39.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "renovate-bug-repro"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "opencv-python-headless" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "opencv-python-headless", specifier = ">=4.8" }]
|
||||
"#
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that we hint if the resolution failed for a different platform.
|
||||
#[test]
|
||||
fn lock_conflict_for_disjoint_platform() -> Result<()> {
|
||||
|
|
|
@ -8140,7 +8140,7 @@ fn sync_dry_run() -> Result<()> {
|
|||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--dry-run"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
@ -8148,14 +8148,15 @@ fn sync_dry_run() -> Result<()> {
|
|||
----- stderr -----
|
||||
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
|
||||
Would replace existing virtual environment at: .venv
|
||||
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'`
|
||||
Resolved 2 packages in [TIME]
|
||||
Would update lockfile at: uv.lock
|
||||
Would install 1 package
|
||||
+ iniconfig==2.0.0
|
||||
"###);
|
||||
");
|
||||
|
||||
// Perform a full sync.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
@ -8164,10 +8165,11 @@ fn sync_dry_run() -> Result<()> {
|
|||
Using CPython 3.9.[X] interpreter at: [PYTHON-3.9]
|
||||
Removed virtual environment at: .venv
|
||||
Creating virtual environment at: .venv
|
||||
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.12'` vs `python_full_version == '3.9.*'`
|
||||
Resolved 2 packages in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
"###);
|
||||
");
|
||||
|
||||
let output = context.sync().arg("--dry-run").arg("-vv").output()?;
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
@ -8658,6 +8660,7 @@ fn sync_locked_script() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Recreating script environment at: [CACHE_DIR]/environments-v2/script-[HASH]
|
||||
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'`
|
||||
Resolved 6 packages in [TIME]
|
||||
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
");
|
||||
|
@ -8669,6 +8672,7 @@ fn sync_locked_script() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Using script environment at: [CACHE_DIR]/environments-v2/script-[HASH]
|
||||
warning: Ignoring existing lockfile due to fork markers being disjoint with `requires-python`: `python_full_version >= '3.11'` vs `python_full_version >= '3.8' and python_full_version < '3.11'`
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 6 packages in [TIME]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue