mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
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:
parent
d9cd2829fa
commit
970bd1aa0c
3 changed files with 442 additions and 1 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue