From 06baf49e3d7372e50721c1be26079885d9f9de10 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 20 Aug 2024 20:37:37 -0400 Subject: [PATCH] Skip override resolution in lock --- crates/uv-resolver/src/lock.rs | 51 +++++++ ...r__lock__tests__hash_optional_missing.snap | 1 + ...r__lock__tests__hash_optional_present.snap | 1 + ...r__lock__tests__hash_required_present.snap | 1 + ...missing_dependency_source_unambiguous.snap | 1 + ...dependency_source_version_unambiguous.snap | 1 + ...issing_dependency_version_unambiguous.snap | 1 + ...lock__tests__source_direct_has_subdir.snap | 1 + ..._lock__tests__source_direct_no_subdir.snap | 1 + ...solver__lock__tests__source_directory.snap | 1 + ...esolver__lock__tests__source_editable.snap | 1 + crates/uv/src/commands/project/lock.rs | 30 ++-- crates/uv/tests/edit.rs | 3 + crates/uv/tests/lock.rs | 139 ++++++++++++++++++ 14 files changed, 224 insertions(+), 9 deletions(-) diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 812ed47cb07b..768cf6e3550a 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -567,6 +567,26 @@ impl Lock { ); } + if !self.manifest.requirements.is_empty() { + let requirements = self + .manifest + .requirements + .iter() + .map(|requirement| { + serde::Serialize::serialize( + &requirement, + toml_edit::ser::ValueSerializer::new(), + ) + }) + .collect::, _>>()?; + let requirements = match requirements.as_slice() { + [] => Array::new(), + [requirement] => Array::from_iter([requirement]), + requirements => each_element_on_its_line_array(requirements.iter()), + }; + manifest_table.insert("requirements", value(requirements)); + } + if !self.manifest.constraints.is_empty() { let constraints = self .manifest @@ -657,6 +677,7 @@ impl Lock { &self, workspace: &Workspace, members: &[PackageName], + requirements: &[Requirement], constraints: &[Requirement], overrides: &[Requirement], indexes: Option<&IndexLocations>, @@ -679,6 +700,29 @@ impl Lock { } } + // Validate that the lockfile was generated with the same requirements. + { + let expected: BTreeSet<_> = requirements + .iter() + .cloned() + .map(|requirement| normalize_requirement(requirement, workspace)) + .collect::>()?; + let actual: BTreeSet<_> = self + .manifest + .requirements + .iter() + .cloned() + .map(|requirement| normalize_requirement(requirement, workspace)) + .collect::>()?; + if expected != actual { + debug!( + "Mismatched requirements:\n expected: {:?}\n found: {:?}", + expected, actual + ); + return Ok(SatisfiesResult::MismatchedConstraints(expected, actual)); + } + } + // Validate that the lockfile was generated with the same constraints. { let expected: BTreeSet<_> = constraints @@ -901,6 +945,8 @@ pub enum SatisfiesResult<'lock> { Satisfied, /// The lockfile uses a different set of workspace members. MismatchedMembers(BTreeSet, &'lock BTreeSet), + /// The lockfile uses a different set of requirements. + MismatchedRequirements(BTreeSet, BTreeSet), /// The lockfile uses a different set of constraints. MismatchedConstraints(BTreeSet, BTreeSet), /// The lockfile uses a different set of overrides. @@ -947,6 +993,9 @@ pub struct ResolverManifest { /// The workspace members included in the lockfile. #[serde(default)] members: BTreeSet, + /// The requirements provided to the resolver, exclusive of the workspace members. + #[serde(default)] + requirements: BTreeSet, /// The constraints provided to the resolver. #[serde(default)] constraints: BTreeSet, @@ -958,11 +1007,13 @@ pub struct ResolverManifest { impl ResolverManifest { pub fn new( members: impl IntoIterator, + requirements: impl IntoIterator, constraints: impl IntoIterator, overrides: impl IntoIterator, ) -> Self { Self { members: members.into_iter().collect(), + requirements: requirements.into_iter().collect(), constraints: constraints.into_iter().collect(), overrides: overrides.into_iter().collect(), } diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap index dc681ebe3405..f5a25df78a97 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap @@ -79,6 +79,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_present.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_present.snap index 1adc09b7480f..3ad72e2ee30e 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_present.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_present.snap @@ -86,6 +86,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_required_present.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_required_present.snap index d2a909d67c30..a836f45002d7 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_required_present.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_required_present.snap @@ -82,6 +82,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap index 3d98ecdff8ea..52e0bdd962f6 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap @@ -141,6 +141,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap index 3d98ecdff8ea..52e0bdd962f6 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap @@ -141,6 +141,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap index 3d98ecdff8ea..52e0bdd962f6 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap @@ -141,6 +141,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap index 1c15e7a0f2f8..587699246676 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap @@ -63,6 +63,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap index 520f1d927576..fa21b8f563ab 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap @@ -59,6 +59,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_directory.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_directory.snap index 8fd5bda01232..a06debad0c11 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_directory.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_directory.snap @@ -49,6 +49,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_editable.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_editable.snap index 468c047661c1..4117dbf996e7 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_editable.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__source_editable.snap @@ -49,6 +49,7 @@ Ok( }, manifest: ResolverManifest { members: {}, + requirements: {}, constraints: {}, overrides: {}, }, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index a51d3b22894d..363420ff70f1 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -249,11 +249,8 @@ async fn do_lock( sources, } = settings; - // When locking, include the project itself (as editable). - let requirements = workspace - .members_requirements() - .chain(workspace.root_requirements()) - .collect::>(); + // Collect the requirements, etc. + let requirements = workspace.root_requirements().collect::>(); let overrides = workspace.overrides().into_iter().collect::>(); let constraints = workspace.constraints(); let dev = vec![DEV_DEPENDENCIES.clone()]; @@ -413,6 +410,7 @@ async fn do_lock( existing_lock, workspace, &members, + &requirements, &constraints, &overrides, environments, @@ -486,9 +484,9 @@ async fn do_lock( // Resolve the requirements. let resolution = pip::operations::resolve( - requirements - .iter() - .cloned() + workspace + .members_requirements() + .chain(requirements.iter().cloned()) .map(UnresolvedRequirementSpecification::from) .collect(), constraints.clone(), @@ -529,7 +527,12 @@ async fn do_lock( let previous = existing_lock.map(ValidatedLock::into_lock); let lock = Lock::from_resolution_graph(&resolution)? - .with_manifest(ResolverManifest::new(members, constraints, overrides)) + .with_manifest(ResolverManifest::new( + members, + requirements, + constraints, + overrides, + )) .with_supported_environments( environments .cloned() @@ -559,6 +562,7 @@ impl ValidatedLock { lock: Lock, workspace: &Workspace, members: &[PackageName], + requirements: &[Requirement], constraints: &[Requirement], overrides: &[Requirement], environments: Option<&SupportedEnvironments>, @@ -679,6 +683,7 @@ impl ValidatedLock { .satisfies( workspace, members, + requirements, constraints, overrides, indexes, @@ -698,6 +703,13 @@ impl ValidatedLock { ); Ok(Self::Preferable(lock)) } + SatisfiesResult::MismatchedRequirements(expected, actual) => { + debug!( + "Ignoring existing lockfile due to mismatched requirements:\n Expected: {:?}\n Actual: {:?}", + expected, actual + ); + Ok(Self::Preferable(lock)) + } SatisfiesResult::MismatchedConstraints(expected, actual) => { debug!( "Ignoring existing lockfile due to mismatched constraints:\n Expected: {:?}\n Actual: {:?}", diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index df34abc3c60e..f9f0156f9bcf 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -3227,6 +3227,9 @@ fn add_virtual() -> Result<()> { [options] exclude-newer = "2024-03-25T00:00:00Z" + [manifest] + requirements = [{ name = "iniconfig" }] + [[package]] name = "iniconfig" version = "2.0.0" diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index cb0948c36cf8..ce941a0b2b03 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -10027,3 +10027,142 @@ fn lock_overlapping_environment() -> Result<()> { Ok(()) } + +/// Lock a requirement from PyPI. +#[test] +fn lock_virtual() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [tool.uv.workspace] + members = [] + + [tool.uv] + dev-dependencies = [ + "anyio" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + requirements = [{ name = "anyio" }] + + [[package]] + name = "anyio" + version = "4.3.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + // Add `iniconfig`. + pyproject_toml.write_str( + r#" + [tool.uv.workspace] + members = [] + + [tool.uv] + dev-dependencies = [ + "anyio", + "iniconfig" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Added iniconfig v2.0.0 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "###); + + Ok(()) +}