Skip to content

Commit

Permalink
Add some additional tests for extras validation in uv sync (#11495)
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh authored Feb 13, 2025
1 parent a8b5d97 commit 71bda82
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 7 deletions.
18 changes: 11 additions & 7 deletions crates/uv/src/commands/project/install_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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()))
Expand Down
155 changes: 155 additions & 0 deletions crates/uv/tests/it/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 71bda82

Please sign in to comment.