mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-02 04:48:18 +00:00
Ignore hidden directories in workspace discovery (#5408)
## Summary This is surprisingly complex because we need to decide what happens if you run `uv run` from within a hidden folder, etc. For now, I did the simplest thing: we just ignore workspace members that are hidden directories if they lack a `pyproject.toml`, so you can still include hidden members, they're just ignored if they don't seem to be projects. Closes https://github.com/astral-sh/uv/issues/5403.
This commit is contained in:
parent
22db997240
commit
9c9510c9ef
3 changed files with 220 additions and 11 deletions
|
|
@ -21,6 +21,8 @@ pub enum WorkspaceError {
|
||||||
// Workspace structure errors.
|
// Workspace structure errors.
|
||||||
#[error("No `pyproject.toml` found in current directory or any parent directory")]
|
#[error("No `pyproject.toml` found in current directory or any parent directory")]
|
||||||
MissingPyprojectToml,
|
MissingPyprojectToml,
|
||||||
|
#[error("Workspace member `{}` is missing a `pyproject.toml` (matches: `{1}`)", _0.simplified_display())]
|
||||||
|
MissingPyprojectTomlMember(PathBuf, String),
|
||||||
#[error("No `project` table found in: `{}`", _0.simplified_display())]
|
#[error("No `project` table found in: `{}`", _0.simplified_display())]
|
||||||
MissingProject(PathBuf),
|
MissingProject(PathBuf),
|
||||||
#[error("No workspace found for: `{}`", _0.simplified_display())]
|
#[error("No workspace found for: `{}`", _0.simplified_display())]
|
||||||
|
|
@ -394,7 +396,7 @@ impl Workspace {
|
||||||
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Adding root workspace member: {}",
|
"Adding root workspace member: `{}`",
|
||||||
workspace_root.simplified_display()
|
workspace_root.simplified_display()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -413,7 +415,7 @@ impl Workspace {
|
||||||
// The current project is a workspace member, especially in a single project workspace.
|
// The current project is a workspace member, especially in a single project workspace.
|
||||||
if let Some(root_member) = current_project {
|
if let Some(root_member) = current_project {
|
||||||
debug!(
|
debug!(
|
||||||
"Adding current workspace member: {}",
|
"Adding current workspace member: `{}`",
|
||||||
root_member.root.simplified_display()
|
root_member.root.simplified_display()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -436,6 +438,11 @@ impl Workspace {
|
||||||
if !seen.insert(member_root.clone()) {
|
if !seen.insert(member_root.clone()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let member_root = absolutize_path(&member_root)
|
||||||
|
.map_err(WorkspaceError::Normalize)?
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
// If the directory is explicitly ignored, skip it.
|
||||||
if options.ignore.contains(member_root.as_path()) {
|
if options.ignore.contains(member_root.as_path()) {
|
||||||
debug!(
|
debug!(
|
||||||
"Ignoring workspace member: `{}`",
|
"Ignoring workspace member: `{}`",
|
||||||
|
|
@ -443,15 +450,38 @@ impl Workspace {
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let member_root = absolutize_path(&member_root)
|
|
||||||
.map_err(WorkspaceError::Normalize)?
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
trace!("Processing workspace member {}", member_root.user_display());
|
trace!(
|
||||||
|
"Processing workspace member: `{}`",
|
||||||
|
member_root.user_display()
|
||||||
|
);
|
||||||
|
|
||||||
// Read the member `pyproject.toml`.
|
// Read the member `pyproject.toml`.
|
||||||
let pyproject_path = member_root.join("pyproject.toml");
|
let pyproject_path = member_root.join("pyproject.toml");
|
||||||
let contents = fs_err::tokio::read_to_string(&pyproject_path).await?;
|
let contents = match fs_err::tokio::read_to_string(&pyproject_path).await {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
// If the directory is hidden, skip it.
|
||||||
|
if member_root
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.as_encoded_bytes().starts_with(b"."))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
debug!(
|
||||||
|
"Ignoring hidden workspace member: `{}`",
|
||||||
|
member_root.simplified_display()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(WorkspaceError::MissingPyprojectTomlMember(
|
||||||
|
member_root,
|
||||||
|
member_glob.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
|
||||||
let pyproject_toml = PyProjectToml::from_string(contents)
|
let pyproject_toml = PyProjectToml::from_string(contents)
|
||||||
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))?;
|
||||||
|
|
||||||
|
|
@ -476,7 +506,7 @@ impl Workspace {
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Adding discovered workspace member: {}",
|
"Adding discovered workspace member: `{}`",
|
||||||
member_root.simplified_display()
|
member_root.simplified_display()
|
||||||
);
|
);
|
||||||
workspace_members.insert(
|
workspace_members.insert(
|
||||||
|
|
@ -869,7 +899,7 @@ async fn find_workspace(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
trace!(
|
trace!(
|
||||||
"Found pyproject.toml: {}",
|
"Found `pyproject.toml` at: `{}`",
|
||||||
pyproject_path.simplified_display()
|
pyproject_path.simplified_display()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -886,7 +916,7 @@ async fn find_workspace(
|
||||||
{
|
{
|
||||||
if is_excluded_from_workspace(project_root, workspace_root, workspace)? {
|
if is_excluded_from_workspace(project_root, workspace_root, workspace)? {
|
||||||
debug!(
|
debug!(
|
||||||
"Found workspace root `{}`, but project is excluded.",
|
"Found workspace root `{}`, but project is excluded",
|
||||||
workspace_root.simplified_display()
|
workspace_root.simplified_display()
|
||||||
);
|
);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|
@ -925,7 +955,7 @@ async fn find_workspace(
|
||||||
} else {
|
} else {
|
||||||
// We require that a `project.toml` file either declares a workspace or a project.
|
// We require that a `project.toml` file either declares a workspace or a project.
|
||||||
warn!(
|
warn!(
|
||||||
"pyproject.toml does not contain `project` table: `{}`",
|
"`pyproject.toml` does not contain a `project` table: `{}`",
|
||||||
pyproject_path.simplified_display()
|
pyproject_path.simplified_display()
|
||||||
);
|
);
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|
|
||||||
|
|
@ -1045,3 +1045,18 @@ fn init_unmanaged() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_hidden() {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.init().arg(".foo"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv init` is experimental and may change without warning
|
||||||
|
error: Not a valid package or extra name: ".foo". Names must start and end with a letter or digit and may only contain -, _, ., and alphanumeric characters.
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -690,3 +690,167 @@ fn workspace_to_workspace_paths_dependencies() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure that workspace discovery errors if a member is missing a `pyproject.toml`.
|
||||||
|
#[test]
|
||||||
|
fn workspace_empty_member() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
// Build the main workspace ...
|
||||||
|
let workspace = context.temp_dir.child("workspace");
|
||||||
|
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["packages/*"]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// ... with a ...
|
||||||
|
let deps = indoc! {r#"
|
||||||
|
dependencies = ["b"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
b = { workspace = true }
|
||||||
|
"#};
|
||||||
|
make_project(&workspace.join("packages").join("a"), "a", deps)?;
|
||||||
|
|
||||||
|
// ... and b.
|
||||||
|
let deps = indoc! {r"
|
||||||
|
"};
|
||||||
|
make_project(&workspace.join("packages").join("b"), "b", deps)?;
|
||||||
|
|
||||||
|
// ... and an empty c.
|
||||||
|
fs_err::create_dir_all(workspace.join("packages").join("c"))?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&workspace), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Workspace member `[TEMP_DIR]/workspace/packages/c` is missing a `pyproject.toml` (matches: `packages/*`)
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that workspace discovery ignores hidden directories.
|
||||||
|
#[test]
|
||||||
|
fn workspace_hidden_files() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
// Build the main workspace ...
|
||||||
|
let workspace = context.temp_dir.child("workspace");
|
||||||
|
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["packages/*"]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// ... with a ...
|
||||||
|
let deps = indoc! {r#"
|
||||||
|
dependencies = ["b"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
b = { workspace = true }
|
||||||
|
"#};
|
||||||
|
make_project(&workspace.join("packages").join("a"), "a", deps)?;
|
||||||
|
|
||||||
|
// ... and b.
|
||||||
|
let deps = indoc! {r"
|
||||||
|
"};
|
||||||
|
make_project(&workspace.join("packages").join("b"), "b", deps)?;
|
||||||
|
|
||||||
|
// ... and a hidden c.
|
||||||
|
fs_err::create_dir_all(workspace.join("packages").join(".c"))?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&workspace), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||||
|
Resolved 2 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
|
||||||
|
|
||||||
|
assert_json_snapshot!(lock.sources(), @r###"
|
||||||
|
{
|
||||||
|
"a": {
|
||||||
|
"editable": "packages/a"
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"editable": "packages/b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that workspace discovery accepts valid hidden directories.
|
||||||
|
#[test]
|
||||||
|
fn workspace_hidden_member() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
// Build the main workspace ...
|
||||||
|
let workspace = context.temp_dir.child("workspace");
|
||||||
|
workspace.child("pyproject.toml").write_str(indoc! {r#"
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["packages/*"]
|
||||||
|
"#})?;
|
||||||
|
|
||||||
|
// ... with a ...
|
||||||
|
let deps = indoc! {r#"
|
||||||
|
dependencies = ["b"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
b = { workspace = true }
|
||||||
|
"#};
|
||||||
|
make_project(&workspace.join("packages").join("a"), "a", deps)?;
|
||||||
|
|
||||||
|
// ... and b.
|
||||||
|
let deps = indoc! {r#"
|
||||||
|
dependencies = ["c"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
c = { workspace = true }
|
||||||
|
"#};
|
||||||
|
make_project(&workspace.join("packages").join("b"), "b", deps)?;
|
||||||
|
|
||||||
|
// ... and a hidden (but valid) .c.
|
||||||
|
let deps = indoc! {r"
|
||||||
|
dependencies = []
|
||||||
|
"};
|
||||||
|
make_project(&workspace.join("packages").join(".c"), "c", deps)?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&workspace), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
let lock: SourceLock = toml::from_str(&fs_err::read_to_string(workspace.join("uv.lock"))?)?;
|
||||||
|
|
||||||
|
assert_json_snapshot!(lock.sources(), @r###"
|
||||||
|
{
|
||||||
|
"a": {
|
||||||
|
"editable": "packages/a"
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"editable": "packages/b"
|
||||||
|
},
|
||||||
|
"c": {
|
||||||
|
"editable": "packages/.c"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue