diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index ca1a51047..65f3927ab 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -330,6 +330,21 @@ impl Workspace { &self.pyproject_toml } + /// Returns `true` if the path is excluded by the workspace. + pub fn excludes(&self, project_path: &Path) -> Result { + if let Some(workspace) = self + .pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.workspace.as_ref()) + { + is_excluded_from_workspace(project_path, &self.install_path, workspace) + } else { + Ok(false) + } + } + /// Collect the workspace member projects from the `members` and `excludes` entries. async fn collect_members( workspace_root: PathBuf, diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index dd59ddc45..f0267575a 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -115,22 +115,32 @@ pub(crate) async fn init( } if let Some(workspace) = workspace { - // Add the package to the workspace. - let mut pyproject = PyProjectTomlMut::from_toml(workspace.pyproject_toml())?; - pyproject.add_workspace(path.strip_prefix(workspace.install_path())?)?; + if workspace.excludes(&path)? { + // If the member is excluded by the workspace, ignore it. + writeln!( + printer.stderr(), + "Project `{}` is excluded by workspace `{}`", + name.cyan(), + workspace.install_path().simplified_display().cyan() + )?; + } else { + // Add the package to the workspace. + let mut pyproject = PyProjectTomlMut::from_toml(workspace.pyproject_toml())?; + pyproject.add_workspace(path.strip_prefix(workspace.install_path())?)?; - // Save the modified `pyproject.toml`. - fs_err::write( - workspace.install_path().join("pyproject.toml"), - pyproject.to_string(), - )?; + // Save the modified `pyproject.toml`. + fs_err::write( + workspace.install_path().join("pyproject.toml"), + pyproject.to_string(), + )?; - writeln!( - printer.stderr(), - "Adding `{}` as member of workspace `{}`", - name.cyan(), - workspace.install_path().simplified_display().cyan() - )?; + writeln!( + printer.stderr(), + "Adding `{}` as member of workspace `{}`", + name.cyan(), + workspace.install_path().simplified_display().cyan() + )?; + } } match explicit_path { diff --git a/crates/uv/tests/init.rs b/crates/uv/tests/init.rs index f1094a5ca..7c69344a4 100644 --- a/crates/uv/tests/init.rs +++ b/crates/uv/tests/init.rs @@ -727,7 +727,7 @@ fn init_virtual_workspace() -> Result<()> { /// Run `uv init` from within a workspace. The path is already included via `members`. #[test] -fn init_matches_member() -> Result<()> { +fn init_matches_members() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -766,3 +766,47 @@ fn init_matches_member() -> Result<()> { Ok(()) } + +/// Run `uv init` from within a workspace. The path is already included via `members`. +#[test] +fn init_matches_exclude() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { + r" + [tool.uv.workspace] + exclude = ['packages/foo'] + members = ['packages/*'] + ", + })?; + + let packages = context.temp_dir.join("packages"); + fs_err::create_dir_all(packages)?; + + uv_snapshot!(context.filters(), context.init().current_dir(context.temp_dir.join("packages")).arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv init` is experimental and may change without warning + Project `foo` is excluded by workspace `[TEMP_DIR]/` + Initialized project `foo` at `[TEMP_DIR]/packages/foo` + "###); + + let workspace = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + workspace, @r###" + [tool.uv.workspace] + exclude = ['packages/foo'] + members = ['packages/*'] + "### + ); + }); + + Ok(()) +}