Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
12 changes: 10 additions & 2 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3633,7 +3633,8 @@ pub struct AddArgs {
conflicts_with = "dev",
conflicts_with = "optional",
conflicts_with = "package",
conflicts_with = "workspace"
conflicts_with = "workspace",
conflicts_with = "no_workspace"
)]
pub script: Option<PathBuf>,

Expand All @@ -3654,8 +3655,15 @@ pub struct AddArgs {
///
/// When used with a path dependency, the package will be added to the workspace's `members`
/// list in the root `pyproject.toml` file.
#[arg(long)]
#[arg(long, conflicts_with = "no_workspace")]
pub workspace: bool,

/// Prevent adding the dependency as a workspace member.
///
/// By default, uv will add path dependencies within the same source tree as workspace members.
/// Use this flag to add the dependency as a regular path dependency instead.
#[arg(long, conflicts_with = "workspace")]
pub no_workspace: bool,
}

#[derive(Args)]
Expand Down
65 changes: 45 additions & 20 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub(crate) async fn add(
package: Option<PackageName>,
python: Option<String>,
workspace: bool,
no_workspace: bool,
install_mirrors: PythonInstallMirrors,
settings: ResolverInstallerSettings,
network_settings: NetworkSettings,
Expand Down Expand Up @@ -495,16 +496,28 @@ pub(crate) async fn add(
// Track modification status, for reverts.
let mut modified = false;

// If `--workspace` is provided, add any members to the `workspace` section of the
// `pyproject.toml` file.
if workspace {
// Determine if we should add path dependencies as workspace members.
// This is the default behavior for path dependencies within the same source tree,
// unless explicitly disabled with `--no-workspace`.
let should_add_workspace_members = if workspace {
true
} else if no_workspace {
false
} else {
// Default: add as workspace members if the path is within the workspace root
true
};

// If we should add workspace members, add any path dependencies to the `workspace` section
// of the `pyproject.toml` file.
if should_add_workspace_members {
let AddTarget::Project(project, python_target) = target else {
unreachable!("`--workspace` and `--script` are conflicting options");
};

let workspace = project.workspace();
let workspace_obj = project.workspace();
let mut toml = PyProjectTomlMut::from_toml(
&workspace.pyproject_toml().raw,
&workspace_obj.pyproject_toml().raw,
DependencyTarget::PyProjectToml,
)?;

Expand All @@ -517,20 +530,32 @@ pub(crate) async fn add(
project.root().join(install_path)
};

// Check if the path is not already included in the workspace.
if !workspace.includes(&absolute_path)? {
let relative_path = absolute_path
.strip_prefix(workspace.install_path())
.unwrap_or(&absolute_path);

toml.add_workspace(relative_path)?;
modified |= true;

writeln!(
printer.stderr(),
"Added `{}` to workspace members",
relative_path.user_display().cyan()
)?;
// Check if the path is within the workspace root (same source tree)
let is_within_workspace = absolute_path
.strip_prefix(workspace_obj.install_path())
.is_ok();

// Only add to workspace if:
// 1. The path is within the workspace root (same source tree), OR
// 2. --workspace was explicitly provided
let should_add_to_workspace = is_within_workspace || workspace;

if should_add_to_workspace {
// Check if the path is not already included in the workspace.
if !workspace_obj.includes(&absolute_path)? {
let relative_path = absolute_path
.strip_prefix(workspace_obj.install_path())
.unwrap_or(&absolute_path);

toml.add_workspace(relative_path)?;
modified |= true;

writeln!(
printer.stderr(),
"Added `{}` to workspace members",
relative_path.user_display().cyan()
)?;
}
}
}
}
Expand All @@ -540,7 +565,7 @@ pub(crate) async fn add(
target = if modified {
let workspace_content = toml.to_string();
fs_err::write(
workspace.install_path().join("pyproject.toml"),
workspace_obj.install_path().join("pyproject.toml"),
&workspace_content,
)?;

Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,7 @@ async fn run_project(
args.package,
args.python,
args.workspace,
args.no_workspace,
args.install_mirrors,
args.settings,
globals.network_settings,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,7 @@ pub(crate) struct AddSettings {
pub(crate) script: Option<PathBuf>,
pub(crate) python: Option<String>,
pub(crate) workspace: bool,
pub(crate) no_workspace: bool,
pub(crate) install_mirrors: PythonInstallMirrors,
pub(crate) refresh: Refresh,
pub(crate) indexes: Vec<Index>,
Expand Down Expand Up @@ -1365,6 +1366,7 @@ impl AddSettings {
script,
python,
workspace,
no_workspace,
} = args;

let dependency_type = if let Some(extra) = optional {
Expand Down Expand Up @@ -1466,6 +1468,7 @@ impl AddSettings {
script,
python: python.and_then(Maybe::into_option),
workspace,
no_workspace,
editable: flag(editable, no_editable, "editable"),
extras: extra.unwrap_or_default(),
refresh: Refresh::from(refresh),
Expand Down
125 changes: 125 additions & 0 deletions crates/uv/tests/it/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13040,3 +13040,128 @@ fn add_path_with_workspace() -> Result<()> {

Ok(())
}

/// Add a path dependency within the same source tree - should default to workspace member
#[test]
fn add_path_default_workspace_same_source_tree() -> Result<()> {
let context = TestContext::new("3.12");

let workspace_toml = context.temp_dir.child("pyproject.toml");
workspace_toml.write_str(indoc! {r#"
[project]
name = "parent"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
"#})?;

// Create a dependency package within the workspace (same source tree)
let dep_dir = context.temp_dir.child("dep");
dep_dir.create_dir_all()?;

let dep_toml = dep_dir.child("pyproject.toml");
dep_toml.write_str(indoc! {r#"
[project]
name = "dep"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
"#})?;

// Add the dependency without any flags - should default to workspace member
uv_snapshot!(context.filters(), context
.add()
.arg("./dep"), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Added `dep` to workspace members
Resolved 2 packages in [TIME]
Audited in [TIME]
");

let pyproject_toml = context.read("pyproject.toml");
assert_snapshot!(
pyproject_toml, @r#"
[project]
name = "parent"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"dep",
]

[tool.uv.workspace]
members = [
"dep",
]

[tool.uv.sources]
dep = { workspace = true }
"#
);

Ok(())
}

/// Add a path dependency within the same source tree with --no-workspace - should not add to workspace
#[test]
fn add_path_no_workspace_same_source_tree() -> Result<()> {
let context = TestContext::new("3.12");

let workspace_toml = context.temp_dir.child("pyproject.toml");
workspace_toml.write_str(indoc! {r#"
[project]
name = "parent"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
"#})?;

// Create a dependency package within the workspace (same source tree)
let dep_dir = context.temp_dir.child("dep");
dep_dir.create_dir_all()?;

let dep_toml = dep_dir.child("pyproject.toml");
dep_toml.write_str(indoc! {r#"
[project]
name = "dep"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
"#})?;

// Add the dependency with --no-workspace flag - should not add to workspace
uv_snapshot!(context.filters(), context
.add()
.arg("./dep")
.arg("--no-workspace"), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 2 packages in [TIME]
Audited in [TIME]
");

let pyproject_toml = context.read("pyproject.toml");
assert_snapshot!(
pyproject_toml, @r#"
[project]
name = "parent"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"dep",
]

[tool.uv.sources]
dep = { path = "dep" }
"#
);

Ok(())
}
Loading