Skip to content

Detect nested workspace inside the current workspace and members with identical names #9094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/uv-workspace/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub use workspace::{
check_nested_workspaces, DiscoveryOptions, MemberDiscovery, ProjectWorkspace, VirtualProject,
Workspace, WorkspaceError, WorkspaceMember,
DiscoveryOptions, MemberDiscovery, ProjectWorkspace, VirtualProject, Workspace, WorkspaceError,
WorkspaceMember,
};

pub mod dependency_groups;
Expand Down
145 changes: 46 additions & 99 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
use uv_pep508::{MarkerTree, RequirementOrigin, VerbatimUrl};
use uv_pypi_types::{Requirement, RequirementSource, SupportedEnvironments};
use uv_static::EnvVars;
use uv_warnings::{warn_user, warn_user_once};
use uv_warnings::warn_user_once;

use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroups};
use crate::pyproject::{
Expand All @@ -26,14 +26,24 @@ pub enum WorkspaceError {
// Workspace structure errors.
#[error("No `pyproject.toml` found in current directory or any parent directory")]
MissingPyprojectToml,
#[error("Workspace member `{}` is missing a `pyproject.toml` (matches: `{1}`)", _0.simplified_display())]
#[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())]
MissingProject(PathBuf),
#[error("No workspace found for: `{}`", _0.simplified_display())]
MissingWorkspace(PathBuf),
#[error("The project is marked as unmanaged: `{}`", _0.simplified_display())]
NonWorkspace(PathBuf),
#[error("Nested workspaces are not supported, but workspace member (`{}`) has a `uv.workspace` table", _0.simplified_display()
)]
NestedWorkspace(PathBuf),
#[error("Two workspace members are both named: `{name}`: `{}` and `{}`", first.simplified_display(), second.simplified_display())]
DuplicatePackage {
name: PackageName,
first: PathBuf,
second: PathBuf,
},
#[error("pyproject.toml section is declared as dynamic, but must be static: `{0}`")]
DynamicNotAllowed(&'static str),
#[error("Failed to find directories for glob: `{0}`")]
Expand Down Expand Up @@ -185,8 +195,6 @@ impl Workspace {
workspace_root.simplified_display()
);

check_nested_workspaces(&workspace_root, options);

// Unlike in `ProjectWorkspace` discovery, we might be in a legacy non-project root without
// being in any specific project.
let current_project = pyproject_toml
Expand Down Expand Up @@ -626,14 +634,20 @@ impl Workspace {
);

seen.insert(workspace_root.clone());
workspace_members.insert(
if let Some(existing) = workspace_members.insert(
project.name.clone(),
WorkspaceMember {
root: workspace_root.clone(),
project: project.clone(),
pyproject_toml,
},
);
) {
return Err(WorkspaceError::DuplicatePackage {
name: project.name.clone(),
first: existing.root.clone(),
second: workspace_root,
});
}
};
}

Expand Down Expand Up @@ -757,23 +771,47 @@ impl Workspace {
"Adding discovered workspace member: `{}`",
member_root.simplified_display()
);
workspace_members.insert(

if let Some(existing) = workspace_members.insert(
project.name.clone(),
WorkspaceMember {
root: member_root.clone(),
project,
pyproject_toml,
},
);
) {
return Err(WorkspaceError::DuplicatePackage {
name: existing.project.name,
first: existing.root.clone(),
second: member_root,
});
}
}
}

// Test for nested workspaces.
for member in workspace_members.values() {
if member.root() != &workspace_root
&& member
.pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.workspace.as_ref())
.is_some()
{
return Err(WorkspaceError::NestedWorkspace(member.root.clone()));
}
}

let workspace_sources = workspace_pyproject_toml
.tool
.clone()
.and_then(|tool| tool.uv)
.and_then(|uv| uv.sources)
.map(ToolUvSources::into_inner)
.unwrap_or_default();

let workspace_indexes = workspace_pyproject_toml
.tool
.clone()
Expand Down Expand Up @@ -1213,95 +1251,6 @@ async fn find_workspace(
Ok(None)
}

/// Warn when the valid workspace is included in another workspace.
pub fn check_nested_workspaces(inner_workspace_root: &Path, options: &DiscoveryOptions) {
for outer_workspace_root in inner_workspace_root
.ancestors()
.take_while(|path| {
// Only walk up the given directory, if any.
options
.stop_discovery_at
.and_then(Path::parent)
.map(|stop_discovery_at| stop_discovery_at != *path)
.unwrap_or(true)
})
.skip(1)
{
let pyproject_toml_path = outer_workspace_root.join("pyproject.toml");
if !pyproject_toml_path.is_file() {
continue;
}
let contents = match fs_err::read_to_string(&pyproject_toml_path) {
Ok(contents) => contents,
Err(err) => {
warn!(
"Unreadable pyproject.toml `{}`: {err}",
pyproject_toml_path.simplified_display()
);
return;
}
};
let pyproject_toml: PyProjectToml = match toml::from_str(&contents) {
Ok(contents) => contents,
Err(err) => {
warn!(
"Invalid pyproject.toml `{}`: {err}",
pyproject_toml_path.simplified_display()
);
return;
}
};

if let Some(workspace) = pyproject_toml
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.workspace.as_ref())
{
let is_included = match is_included_in_workspace(
inner_workspace_root,
outer_workspace_root,
workspace,
) {
Ok(contents) => contents,
Err(err) => {
warn!(
"Invalid pyproject.toml `{}`: {err}",
pyproject_toml_path.simplified_display()
);
return;
}
};

let is_excluded = match is_excluded_from_workspace(
inner_workspace_root,
outer_workspace_root,
workspace,
) {
Ok(contents) => contents,
Err(err) => {
warn!(
"Invalid pyproject.toml `{}`: {err}",
pyproject_toml_path.simplified_display()
);
return;
}
};

if is_included && !is_excluded {
warn_user!(
"Nested workspaces are not supported, but outer workspace (`{}`) includes `{}`",
outer_workspace_root.simplified_display().cyan(),
inner_workspace_root.simplified_display().cyan()
);
}
}

// We're in the examples or tests of another project (not a workspace), this is fine.
return;
}
}

/// Check if we're in the `tool.uv.workspace.excluded` of a workspace.
fn is_excluded_from_workspace(
project_path: &Path,
Expand Down Expand Up @@ -1414,8 +1363,6 @@ impl VirtualProject {
.map_err(WorkspaceError::Normalize)?
.clone();

check_nested_workspaces(&project_path, options);

let workspace = Workspace::collect_members(
project_path,
workspace.clone(),
Expand Down
Loading
Loading