Respect exclusion when collecting workspace members (#7175)

## Summary

We were only applying exclusions when discovering the root, apparently.

Our logic now matches the original intent, which is...

- `exclude` always post-filters `members`.
- We don't treat globs any differently than non-globs.

The one confusing setup that falls out of this is that given:

```toml
members = ["foo/bar/baz"]
exclude = ["foo/bar"]
```

`foo/bar/baz` **would** be included. To exclude it, you would need:

```toml
members = ["foo/bar/baz"]
exclude = ["foo/bar/*"]
```

Closes https://github.com/astral-sh/uv/issues/7071.
This commit is contained in:
Charlie Marsh 2024-09-09 12:08:06 -04:00 committed by GitHub
parent d9cd2829fa
commit 970bd1aa0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 442 additions and 1 deletions

3
Cargo.lock generated
View file

@ -5225,6 +5225,8 @@ dependencies = [
name = "uv-workspace"
version = "0.0.1"
dependencies = [
"anyhow",
"assert_fs",
"either",
"fs-err",
"glob",
@ -5238,6 +5240,7 @@ dependencies = [
"same-file",
"schemars",
"serde",
"tempfile",
"thiserror",
"tokio",
"toml",

View file

@ -39,8 +39,11 @@ url = { workspace = true }
itertools = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
assert_fs = { version = "1.1.0" }
insta = { version = "1.39.0", features = ["filters", "json", "redactions"] }
regex = { workspace = true }
tempfile = { workspace = true }
[package.metadata.cargo-shear]
ignored = ["uv-options-metadata"]

View file

@ -618,7 +618,7 @@ impl Workspace {
}
// Add all other workspace members.
for member_glob in workspace_definition.members.unwrap_or_default() {
for member_glob in workspace_definition.clone().members.unwrap_or_default() {
let absolute_glob = workspace_root
.simplified()
.join(member_glob.as_str())
@ -650,6 +650,16 @@ impl Workspace {
continue;
}
// If the member is excluded, ignore it.
if is_excluded_from_workspace(&member_root, &workspace_root, &workspace_definition)?
{
debug!(
"Ignoring workspace member: `{}`",
member_root.simplified_display()
);
continue;
}
trace!(
"Processing workspace member: `{}`",
member_root.user_display()
@ -1538,6 +1548,11 @@ impl<'env> From<&'env VirtualProject> for InstallTarget<'env> {
mod tests {
use std::env;
use std::path::Path;
use anyhow::Result;
use assert_fs::fixture::ChildPath;
use assert_fs::prelude::*;
use insta::assert_json_snapshot;
use crate::workspace::{DiscoveryOptions, ProjectWorkspace};
@ -1559,6 +1574,14 @@ mod tests {
(project, root_escaped)
}
async fn temporary_test(folder: &Path) -> (ProjectWorkspace, String) {
let project = ProjectWorkspace::discover(folder, &DiscoveryOptions::default())
.await
.unwrap();
let root_escaped = regex::escape(folder.to_string_lossy().as_ref());
(project, root_escaped)
}
#[tokio::test]
async fn albatross_in_example() {
let (project, root_escaped) =
@ -1856,4 +1879,416 @@ mod tests {
"###);
});
}
#[tokio::test]
async fn exclude_package() -> Result<()> {
let root = tempfile::TempDir::new()?;
let root = ChildPath::new(root.path());
// Create the root.
root.child("pyproject.toml").write_str(
r#"
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["tqdm>=4,<5"]
[tool.uv.workspace]
members = ["packages/*"]
exclude = ["packages/bird-feeder"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
root.child("albatross").child("__init__.py").touch()?;
// Create an included package (`seeds`).
root.child("packages")
.child("seeds")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "seeds"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = ["idna==3.6"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
root.child("packages")
.child("seeds")
.child("seeds")
.child("__init__.py")
.touch()?;
// Create an excluded package (`bird-feeder`).
root.child("packages")
.child("bird-feeder")
.child("pyproject.toml")
.write_str(
r#"
[project]
name = "bird-feeder"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = ["anyio>=4.3.0,<5"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
root.child("packages")
.child("bird-feeder")
.child("bird_feeder")
.child("__init__.py")
.touch()?;
let (project, root_escaped) = temporary_test(root.as_ref()).await;
let filters = vec![(root_escaped.as_str(), "[ROOT]")];
insta::with_settings!({filters => filters}, {
assert_json_snapshot!(
project,
{
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
},
@r###"
{
"project_root": "[ROOT]",
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]",
"packages": {
"albatross": {
"root": "[ROOT]",
"project": {
"name": "albatross",
"version": "0.1.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]"
},
"seeds": {
"root": "[ROOT]/packages/seeds",
"project": {
"name": "seeds",
"version": "1.0.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]"
}
},
"sources": {},
"pyproject_toml": {
"project": {
"name": "albatross",
"version": "0.1.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"tool": {
"uv": {
"sources": null,
"workspace": {
"members": [
"packages/*"
],
"exclude": [
"packages/bird-feeder"
]
},
"managed": null,
"package": null,
"dev-dependencies": null,
"environments": null,
"override-dependencies": null,
"constraint-dependencies": null
}
}
}
}
}
"###);
});
// Rewrite the members to both include and exclude `bird-feeder` by name.
root.child("pyproject.toml").write_str(
r#"
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["tqdm>=4,<5"]
[tool.uv.workspace]
members = ["packages/seeds", "packages/bird-feeder"]
exclude = ["packages/bird-feeder"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
// `bird-feeder` should still be excluded.
let (project, root_escaped) = temporary_test(root.as_ref()).await;
let filters = vec![(root_escaped.as_str(), "[ROOT]")];
insta::with_settings!({filters => filters}, {
assert_json_snapshot!(
project,
{
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
},
@r###"
{
"project_root": "[ROOT]",
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]",
"packages": {
"albatross": {
"root": "[ROOT]",
"project": {
"name": "albatross",
"version": "0.1.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]"
},
"seeds": {
"root": "[ROOT]/packages/seeds",
"project": {
"name": "seeds",
"version": "1.0.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]"
}
},
"sources": {},
"pyproject_toml": {
"project": {
"name": "albatross",
"version": "0.1.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"tool": {
"uv": {
"sources": null,
"workspace": {
"members": [
"packages/seeds",
"packages/bird-feeder"
],
"exclude": [
"packages/bird-feeder"
]
},
"managed": null,
"package": null,
"dev-dependencies": null,
"environments": null,
"override-dependencies": null,
"constraint-dependencies": null
}
}
}
}
}
"###);
});
// Rewrite the exclusion to use the top-level directory (`packages`).
root.child("pyproject.toml").write_str(
r#"
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["tqdm>=4,<5"]
[tool.uv.workspace]
members = ["packages/seeds", "packages/bird-feeder"]
exclude = ["packages"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
// `bird-feeder` should now be included.
let (project, root_escaped) = temporary_test(root.as_ref()).await;
let filters = vec![(root_escaped.as_str(), "[ROOT]")];
insta::with_settings!({filters => filters}, {
assert_json_snapshot!(
project,
{
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
},
@r###"
{
"project_root": "[ROOT]",
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]",
"packages": {
"albatross": {
"root": "[ROOT]",
"project": {
"name": "albatross",
"version": "0.1.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]"
},
"bird-feeder": {
"root": "[ROOT]/packages/bird-feeder",
"project": {
"name": "bird-feeder",
"version": "1.0.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]"
},
"seeds": {
"root": "[ROOT]/packages/seeds",
"project": {
"name": "seeds",
"version": "1.0.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]"
}
},
"sources": {},
"pyproject_toml": {
"project": {
"name": "albatross",
"version": "0.1.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"tool": {
"uv": {
"sources": null,
"workspace": {
"members": [
"packages/seeds",
"packages/bird-feeder"
],
"exclude": [
"packages"
]
},
"managed": null,
"package": null,
"dev-dependencies": null,
"environments": null,
"override-dependencies": null,
"constraint-dependencies": null
}
}
}
}
}
"###);
});
// Rewrite the exclusion to use the top-level directory with a glob (`packages/*`).
root.child("pyproject.toml").write_str(
r#"
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["tqdm>=4,<5"]
[tool.uv.workspace]
members = ["packages/seeds", "packages/bird-feeder"]
exclude = ["packages/*"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#,
)?;
// `bird-feeder` and `seeds` should now be excluded.
let (project, root_escaped) = temporary_test(root.as_ref()).await;
let filters = vec![(root_escaped.as_str(), "[ROOT]")];
insta::with_settings!({filters => filters}, {
assert_json_snapshot!(
project,
{
".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]"
},
@r###"
{
"project_root": "[ROOT]",
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]",
"packages": {
"albatross": {
"root": "[ROOT]",
"project": {
"name": "albatross",
"version": "0.1.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"pyproject_toml": "[PYPROJECT_TOML]"
}
},
"sources": {},
"pyproject_toml": {
"project": {
"name": "albatross",
"version": "0.1.0",
"requires-python": ">=3.12",
"optional-dependencies": null
},
"tool": {
"uv": {
"sources": null,
"workspace": {
"members": [
"packages/seeds",
"packages/bird-feeder"
],
"exclude": [
"packages/*"
]
},
"managed": null,
"package": null,
"dev-dependencies": null,
"environments": null,
"override-dependencies": null,
"constraint-dependencies": null
}
}
}
}
}
"###);
});
Ok(())
}
}