From 19a7f3ec07fc1ac45c2e401a00f789c2099587ea Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Aug 2024 18:36:51 -0400 Subject: [PATCH] Collapse extras on dev dependencies (#6383) ## Summary It turns out we weren't applying the collapse logic here, so dev deps with extras were repeated. This was generally ok... unless we ended up _dropping_ an extra, in which case, you now have a duplicate. Closes https://github.com/astral-sh/uv/issues/6380. --- crates/uv-resolver/src/lock.rs | 26 ++-- crates/uv/tests/lock.rs | 116 +++++++++++++++++- .../ecosystem__transformers-lock-file.snap | 5 - .../ecosystem__warehouse-lock-file.snap | 1 - 4 files changed, 133 insertions(+), 15 deletions(-) diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 952d5b387928..557f6c30706b 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -1154,10 +1154,15 @@ impl Package { annotated_dist: &AnnotatedDist, marker: MarkerTree, ) { - self.optional_dependencies - .entry(extra) - .or_default() - .push(Dependency::from_annotated_dist(annotated_dist, marker)); + let dep = Dependency::from_annotated_dist(annotated_dist, marker); + let optional_deps = self.optional_dependencies.entry(extra).or_default(); + for existing_dep in &mut *optional_deps { + if existing_dep.package_id == dep.package_id && existing_dep.marker == dep.marker { + existing_dep.extra.extend(dep.extra); + return; + } + } + optional_deps.push(dep); } /// Add the [`AnnotatedDist`] as a development dependency of the [`Package`]. @@ -1167,10 +1172,15 @@ impl Package { annotated_dist: &AnnotatedDist, marker: MarkerTree, ) { - self.dev_dependencies - .entry(dev) - .or_default() - .push(Dependency::from_annotated_dist(annotated_dist, marker)); + let dep = Dependency::from_annotated_dist(annotated_dist, marker); + let dev_deps = self.dev_dependencies.entry(dev).or_default(); + for existing_dep in &mut *dev_deps { + if existing_dep.package_id == dep.package_id && existing_dep.marker == dep.marker { + existing_dep.extra.extend(dep.extra); + return; + } + } + dev_deps.push(dep); } /// Convert the [`Package`] to a [`Dist`] that can be used in installation. diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 7e44426bb0f9..0efa6b00fa7f 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -7890,7 +7890,6 @@ fn lock_transitive_extra() -> Result<()> { [package.optional-dependencies] async = [ - { name = "leaf" }, { name = "leaf", extra = ["async"] }, { name = "typing-extensions" }, ] @@ -10306,3 +10305,118 @@ fn lock_virtual() -> Result<()> { Ok(()) } + +/// `coverage` defines a `toml` extra, but it doesn't enable any dependencies after Python 3.11. +#[test] +fn lock_dropped_dev_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" + + [tool.uv] + dev-dependencies = [ + "coverage[toml]" + ] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 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" + + [[package]] + name = "coverage" + version = "7.4.4" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/d5/f809d8b630cf4c11fe490e20037a343d12a74ec2783c6cdb5aee725e7137/coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", size = 783727 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/de/a54b245e781bfd6f3fd7ce5566a695686b5c25ee7c743f514e7634428972/coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", size = 206409 }, + { url = "https://files.pythonhosted.org/packages/88/92/07f9c593cd27e3c595b8cb83b95adad8c9ba3d611debceed097a5fd6be4b/coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", size = 206568 }, + { url = "https://files.pythonhosted.org/packages/41/6d/e142c823e5d4b24481f990da4cf9d2d577a6f4e1fb6faf39d9a4e42b1d43/coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", size = 238920 }, + { url = "https://files.pythonhosted.org/packages/30/1a/105f0139df6a2adbcaa0c110711a46dbd9f59e93a09ca15a97d59c2564f2/coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", size = 236288 }, + { url = "https://files.pythonhosted.org/packages/98/79/185cb42910b6a2b2851980407c8445ac0da0750dff65e420e86f973c8396/coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", size = 238223 }, + { url = "https://files.pythonhosted.org/packages/92/12/2303d1c543a11ea060dbc7144ed3174fc09107b5dd333649415c95ede58b/coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", size = 245161 }, + { url = "https://files.pythonhosted.org/packages/96/5a/7d0e945c4759fe9d19aad1679dd3096aeb4cb9fcf0062fe24554dc4787b8/coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", size = 243066 }, + { url = "https://files.pythonhosted.org/packages/f4/1b/79cdb7b11bbbd6540a536ac79412904b5c1f8903d5c1330084212afa8ceb/coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", size = 244805 }, + { url = "https://files.pythonhosted.org/packages/af/7f/54dc676e7e63549838a3a7b95a8e11df80441bf7d64c6ce8f1cdbc0d1ff0/coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", size = 208590 }, + { url = "https://files.pythonhosted.org/packages/46/c4/1dfe76d96034a347d717a2392b004d42d45934cb94efa362ad41ca871f6e/coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", size = 209415 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + + [package.dev-dependencies] + dev = [ + { name = "coverage" }, + ] + + [package.metadata] + + [package.metadata.requires-dev] + dev = [{ name = "coverage", extras = ["toml"] }] + "### + ); + }); + + // 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] + "###); + + // 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 2 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + coverage==7.4.4 + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + Ok(()) +} diff --git a/crates/uv/tests/snapshots/ecosystem__transformers-lock-file.snap b/crates/uv/tests/snapshots/ecosystem__transformers-lock-file.snap index d4d04a9e983f..6cf9396f52d4 100644 --- a/crates/uv/tests/snapshots/ecosystem__transformers-lock-file.snap +++ b/crates/uv/tests/snapshots/ecosystem__transformers-lock-file.snap @@ -5443,7 +5443,6 @@ all = [ { name = "pillow" }, { name = "protobuf" }, { name = "pyctcdecode" }, - { name = "ray" }, { name = "ray", extra = ["tune"] }, { name = "sentencepiece" }, { name = "sigopt" }, @@ -5542,7 +5541,6 @@ dev-dependencies = [ { name = "pytest" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, - { name = "ray" }, { name = "ray", extra = ["tune"] }, { name = "rhoknp" }, { name = "rjieba" }, @@ -5592,7 +5590,6 @@ docs = [ { name = "pillow" }, { name = "protobuf" }, { name = "pyctcdecode" }, - { name = "ray" }, { name = "ray", extra = ["tune"] }, { name = "sentencepiece" }, { name = "sigopt" }, @@ -5628,7 +5625,6 @@ ftfy = [ ] integrations = [ { name = "optuna" }, - { name = "ray" }, { name = "ray", extra = ["tune"] }, { name = "sigopt" }, ] @@ -5668,7 +5664,6 @@ quality = [ { name = "urllib3" }, ] ray = [ - { name = "ray" }, { name = "ray", extra = ["tune"] }, ] retrieval = [ diff --git a/crates/uv/tests/snapshots/ecosystem__warehouse-lock-file.snap b/crates/uv/tests/snapshots/ecosystem__warehouse-lock-file.snap index 060a94dd7cfc..a6cedab62688 100644 --- a/crates/uv/tests/snapshots/ecosystem__warehouse-lock-file.snap +++ b/crates/uv/tests/snapshots/ecosystem__warehouse-lock-file.snap @@ -491,7 +491,6 @@ wheels = [ [package.optional-dependencies] sqs = [ { name = "boto3" }, - { name = "kombu" }, { name = "kombu", extra = ["sqs"] }, { name = "pycurl", marker = "platform_python_implementation == 'CPython' and sys_platform != 'win32'" }, { name = "urllib3" },