From 58b25b560c0e07d71c7d42c778181f50bbbf1587 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Sep 2024 18:14:57 -0400 Subject: [PATCH] Invalidate lockfile when member versions change (#7102) ## Summary Closes https://github.com/astral-sh/uv/issues/7101. --------- Co-authored-by: Zanie Blue --- crates/uv-resolver/src/lock/mod.rs | 30 +++++++++++- crates/uv-workspace/src/pyproject.rs | 4 +- crates/uv-workspace/src/workspace.rs | 13 +++++ crates/uv/src/commands/project/lock.rs | 12 +++++ crates/uv/tests/sync.rs | 66 ++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 3 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 53b2d6070602..dd2a5071a15d 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -897,9 +897,9 @@ impl Lock { } } - // Validate that the member sources have not changed (e.g., switched from packaged to - // virtual). + // Validate that the member sources have not changed. { + // E.g., that they've switched from virtual to non-virtual or vice versa. for (name, member) in workspace.packages() { let expected = !member.pyproject_toml().is_package(); let actual = self @@ -911,6 +911,30 @@ impl Lock { return Ok(SatisfiesResult::MismatchedSources(name.clone(), expected)); } } + + // E.g., that the version has changed. + for (name, member) in workspace.packages() { + let Some(expected) = member + .pyproject_toml() + .project + .as_ref() + .and_then(|project| project.version.as_ref()) + else { + continue; + }; + let actual = self + .find_by_name(name) + .ok() + .flatten() + .map(|package| &package.id.version); + if actual.map_or(true, |actual| actual != expected) { + return Ok(SatisfiesResult::MismatchedVersion( + name.clone(), + expected.clone(), + actual.cloned(), + )); + } + } } // Validate that the lockfile was generated with the same requirements. @@ -1194,6 +1218,8 @@ pub enum SatisfiesResult<'lock> { MismatchedMembers(BTreeSet, &'lock BTreeSet), /// The lockfile uses a different set of sources for its workspace members. MismatchedSources(PackageName, bool), + /// The lockfile uses a different set of version for its workspace members. + MismatchedVersion(PackageName, Version, Option), /// The lockfile uses a different set of requirements. MismatchedRequirements(BTreeSet, BTreeSet), /// The lockfile uses a different set of constraints. diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 76ba81d44d31..6cc0d2a0c18c 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; -use pep440_rs::VersionSpecifiers; +use pep440_rs::{Version, VersionSpecifiers}; use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; use uv_fs::relative_to; use uv_git::GitReference; @@ -87,6 +87,8 @@ impl AsRef<[u8]> for PyProjectToml { pub struct Project { /// The name of the project pub name: PackageName, + /// The version of the project + pub version: Option, /// The Python versions this project is compatible with. pub requires_python: Option, /// The optional dependencies of the project. diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index d2786fd66126..232bcc18b6b6 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1581,6 +1581,7 @@ mod tests { "root": "[ROOT]/albatross-in-example/examples/bird-feeder", "project": { "name": "bird-feeder", + "version": "1.0.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1591,6 +1592,7 @@ mod tests { "pyproject_toml": { "project": { "name": "bird-feeder", + "version": "1.0.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1624,6 +1626,7 @@ mod tests { "root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", "project": { "name": "bird-feeder", + "version": "1.0.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1634,6 +1637,7 @@ mod tests { "pyproject_toml": { "project": { "name": "bird-feeder", + "version": "1.0.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1666,6 +1670,7 @@ mod tests { "root": "[ROOT]/albatross-root-workspace", "project": { "name": "albatross", + "version": "0.1.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1675,6 +1680,7 @@ mod tests { "root": "[ROOT]/albatross-root-workspace/packages/bird-feeder", "project": { "name": "bird-feeder", + "version": "1.0.0", "requires-python": ">=3.8", "optional-dependencies": null }, @@ -1684,6 +1690,7 @@ mod tests { "root": "[ROOT]/albatross-root-workspace/packages/seeds", "project": { "name": "seeds", + "version": "1.0.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1698,6 +1705,7 @@ mod tests { "pyproject_toml": { "project": { "name": "albatross", + "version": "0.1.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1751,6 +1759,7 @@ mod tests { "root": "[ROOT]/albatross-virtual-workspace/packages/albatross", "project": { "name": "albatross", + "version": "0.1.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1760,6 +1769,7 @@ mod tests { "root": "[ROOT]/albatross-virtual-workspace/packages/bird-feeder", "project": { "name": "bird-feeder", + "version": "1.0.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1769,6 +1779,7 @@ mod tests { "root": "[ROOT]/albatross-virtual-workspace/packages/seeds", "project": { "name": "seeds", + "version": "1.0.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1823,6 +1834,7 @@ mod tests { "root": "[ROOT]/albatross-just-project", "project": { "name": "albatross", + "version": "0.1.0", "requires-python": ">=3.12", "optional-dependencies": null }, @@ -1833,6 +1845,7 @@ mod tests { "pyproject_toml": { "project": { "name": "albatross", + "version": "0.1.0", "requires-python": ">=3.12", "optional-dependencies": null }, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index f9af474c4975..9b04f933d700 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -739,6 +739,18 @@ impl ValidatedLock { } Ok(Self::Preferable(lock)) } + SatisfiesResult::MismatchedVersion(name, expected, actual) => { + if let Some(actual) = actual { + debug!( + "Ignoring existing lockfile due to mismatched version: `{name}` (expected: `{expected}`, found: `{actual}`)" + ); + } else { + debug!( + "Ignoring existing lockfile due to mismatched version: `{name}` (expected: `{expected}`)" + ); + } + Ok(Self::Preferable(lock)) + } SatisfiesResult::MismatchedRequirements(expected, actual) => { debug!( "Ignoring existing lockfile due to mismatched requirements:\n Expected: {:?}\n Actual: {:?}", diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 8b558b672824..18cb5f7af230 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -1892,6 +1892,72 @@ fn sync_virtual_env_warning() -> Result<()> { Ok(()) } +#[test] +fn sync_update_project() -> Result<()> { + let context = TestContext::new_with_versions(&["3.12"]); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "my-project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtualenv at: .venv + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + my-project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + // Bump the project version. + pyproject_toml.write_str( + r#" + [project] + name = "my-project" + version = "0.2.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - my-project==0.1.0 (from file://[TEMP_DIR]/) + + my-project==0.2.0 (from file://[TEMP_DIR]/) + "###); + + Ok(()) +} + #[test] fn sync_environment_prompt() -> Result<()> { let context = TestContext::new_with_versions(&["3.12"]);