Add some additional tests for extras validation in uv sync (#11495)

This commit is contained in:
Charlie Marsh
2025-02-13 18:36:02 -05:00
committed by GitHub
parent a8b5d975b3
commit 71bda82b08
2 changed files with 166 additions and 7 deletions
@@ -2,7 +2,8 @@ use std::borrow::Cow;
use std::path::Path;
use std::str::FromStr;
use itertools::{Either, Itertools};
use itertools::Either;
use rustc_hash::FxHashSet;
use uv_configuration::ExtrasSpecification;
use uv_distribution_types::Index;
@@ -258,10 +259,11 @@ impl<'lock> InstallTarget<'lock> {
Self::Project { lock, .. }
| Self::Workspace { lock, .. }
| Self::NonProjectWorkspace { lock, .. } => {
let roots = self.roots().collect::<FxHashSet<_>>();
let member_packages: Vec<&Package> = lock
.packages()
.iter()
.filter(|package| self.roots().contains(package.name()))
.filter(|package| roots.contains(package.name()))
.collect();
// If `provides-extra` is not set in any package, do not perform the check, as this
@@ -274,12 +276,14 @@ impl<'lock> InstallTarget<'lock> {
return Ok(());
}
// Collect all known extras from the member packages.
let known_extras = member_packages
.iter()
.flat_map(|package| package.provides_extras().into_iter().flatten())
.collect::<FxHashSet<_>>();
for extra in extras {
if !member_packages.iter().any(|package| {
package
.provides_extras()
.is_some_and(|provides_extras| provides_extras.contains(extra))
}) {
if !known_extras.contains(extra) {
return match self {
Self::Project { .. } => {
Err(ProjectError::MissingExtraProject(extra.clone()))
+155
View File
@@ -2771,6 +2771,161 @@ fn sync_ignore_extras_check_when_no_provides_extras() -> Result<()> {
Ok(())
}
#[test]
fn sync_non_existent_extra_workspace_member() -> 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"
dependencies = ["child"]
[project.optional-dependencies]
types = ["sniffio>1"]
[tool.uv.workspace]
members = ["child"]
[tool.uv.sources]
child = { workspace = true }
"#,
)?;
context
.temp_dir
.child("child")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
[project.optional-dependencies]
async = ["anyio>3"]
"#,
)?;
context.lock().assert().success();
// Requesting an extra that only exists in the child should fail.
uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("async"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
error: Extra `async` is not defined in the project's `optional-dependencies` table
"###);
// Unless we sync from the child directory.
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--extra").arg("async"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
"###);
Ok(())
}
#[test]
fn sync_non_existent_extra_non_project_workspace() -> 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 = ["child", "other"]
"#,
)?;
context
.temp_dir
.child("child")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
[project.optional-dependencies]
async = ["anyio>3"]
"#,
)?;
context
.temp_dir
.child("other")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "other"
version = "0.1.0"
requires-python = ">=3.12"
"#,
)?;
context.lock().assert().success();
// Requesting an extra that only exists in the child should succeed, since we sync all members
// by default.
uv_snapshot!(context.filters(), context.sync().arg("--extra").arg("async"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
"###);
// Syncing from the child should also succeed.
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--extra").arg("async"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
Audited 3 packages in [TIME]
"###);
// Syncing from an unrelated child should fail.
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("other").arg("--extra").arg("async"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Resolved 5 packages in [TIME]
error: Extra `async` is not defined in the project's `optional-dependencies` table
"###);
Ok(())
}
/// Regression test for <https://github.com/astral-sh/uv/issues/6316>.
///
/// Previously, we would read metadata statically from pyproject.toml and write that to `uv.lock`. In