Skip to content

Commit c3d7d38

Browse files
charliermarshzanieb
authored andcommitted
Default to --workspace when adding subdirectories (#14529)
If `--workspace` is provided, we add all paths as workspace members. If `--no-workspace` is provided, we add all paths as direct path dependencies. If neither is provided, then we add any paths that are under the workspace root as workspace members, and the rest as direct path dependencies. Closes #14524.
1 parent e4c04af commit c3d7d38

File tree

5 files changed

+521
-41
lines changed

5 files changed

+521
-41
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3726,10 +3726,19 @@ pub struct AddArgs {
37263726

37273727
/// Add the dependency as a workspace member.
37283728
///
3729-
/// When used with a path dependency, the package will be added to the workspace's `members`
3730-
/// list in the root `pyproject.toml` file.
3731-
#[arg(long)]
3729+
/// By default, uv will add path dependencies that are within the workspace directory
3730+
/// as workspace members. When used with a path dependency, the package will be added
3731+
/// to the workspace's `members` list in the root `pyproject.toml` file.
3732+
#[arg(long, overrides_with = "no_workspace")]
37323733
pub workspace: bool,
3734+
3735+
/// Don't add the dependency as a workspace member.
3736+
///
3737+
/// By default, when adding a dependency that's a local path and is within the workspace
3738+
/// directory, uv will add it as a workspace member; pass `--no-workspace` to add the package
3739+
/// as direct path dependency instead.
3740+
#[arg(long, overrides_with = "workspace")]
3741+
pub no_workspace: bool,
37333742
}
37343743

37353744
#[derive(Args)]

crates/uv/src/commands/project/add.rs

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ pub(crate) async fn add(
8383
extras_of_dependency: Vec<ExtraName>,
8484
package: Option<PackageName>,
8585
python: Option<String>,
86-
workspace: bool,
86+
workspace: Option<bool>,
8787
install_mirrors: PythonInstallMirrors,
8888
settings: ResolverInstallerSettings,
8989
network_settings: NetworkSettings,
@@ -497,16 +497,41 @@ pub(crate) async fn add(
497497
// Track modification status, for reverts.
498498
let mut modified = false;
499499

500-
// If `--workspace` is provided, add any members to the `workspace` section of the
500+
// Determine whether to use workspace mode.
501+
let use_workspace = match workspace {
502+
Some(workspace) => workspace,
503+
None => {
504+
// Check if we're in a project (not a script), and if any requirements are path
505+
// dependencies within the workspace.
506+
if let AddTarget::Project(ref project, _) = target {
507+
let workspace_root = project.workspace().install_path();
508+
requirements.iter().any(|req| {
509+
if let RequirementSource::Directory { install_path, .. } = &req.source {
510+
let absolute_path = if install_path.is_absolute() {
511+
install_path.to_path_buf()
512+
} else {
513+
project.root().join(install_path)
514+
};
515+
absolute_path.starts_with(workspace_root)
516+
} else {
517+
false
518+
}
519+
})
520+
} else {
521+
false
522+
}
523+
}
524+
};
525+
526+
// If workspace mode is enabled, add any members to the `workspace` section of the
501527
// `pyproject.toml` file.
502-
if workspace {
528+
if use_workspace {
503529
let AddTarget::Project(project, python_target) = target else {
504530
unreachable!("`--workspace` and `--script` are conflicting options");
505531
};
506532

507-
let workspace = project.workspace();
508533
let mut toml = PyProjectTomlMut::from_toml(
509-
&workspace.pyproject_toml().raw,
534+
&project.workspace().pyproject_toml().raw,
510535
DependencyTarget::PyProjectToml,
511536
)?;
512537

@@ -519,21 +544,32 @@ pub(crate) async fn add(
519544
project.root().join(install_path)
520545
};
521546

522-
// Check if the path is not already included in the workspace.
523-
if !workspace.includes(&absolute_path)? {
524-
let relative_path = absolute_path
525-
.strip_prefix(workspace.install_path())
526-
.unwrap_or(&absolute_path);
527-
528-
toml.add_workspace(relative_path)?;
529-
modified |= true;
547+
// Either `--workspace` was provided explicitly, or it was omitted but the path is
548+
// within the workspace root.
549+
let use_workspace = workspace.unwrap_or_else(|| {
550+
absolute_path.starts_with(project.workspace().install_path())
551+
});
552+
if !use_workspace {
553+
continue;
554+
}
530555

531-
writeln!(
532-
printer.stderr(),
533-
"Added `{}` to workspace members",
534-
relative_path.user_display().cyan()
535-
)?;
556+
// If the project is already a member of the workspace, skip it.
557+
if project.workspace().includes(&absolute_path)? {
558+
continue;
536559
}
560+
561+
let relative_path = absolute_path
562+
.strip_prefix(project.workspace().install_path())
563+
.unwrap_or(&absolute_path);
564+
565+
toml.add_workspace(relative_path)?;
566+
modified |= true;
567+
568+
writeln!(
569+
printer.stderr(),
570+
"Added `{}` to workspace members",
571+
relative_path.user_display().cyan()
572+
)?;
537573
}
538574
}
539575

@@ -542,7 +578,7 @@ pub(crate) async fn add(
542578
target = if modified {
543579
let workspace_content = toml.to_string();
544580
fs_err::write(
545-
workspace.install_path().join("pyproject.toml"),
581+
project.workspace().install_path().join("pyproject.toml"),
546582
&workspace_content,
547583
)?;
548584

@@ -747,13 +783,13 @@ fn edits(
747783
.and_then(|tool| tool.uv.as_ref())
748784
.and_then(|uv| uv.sources.as_ref())
749785
.map(ToolUvSources::inner);
750-
let workspace = project
786+
let is_workspace_member = project
751787
.workspace()
752788
.packages()
753789
.contains_key(&requirement.name);
754790
resolve_requirement(
755791
requirement,
756-
workspace,
792+
is_workspace_member,
757793
editable,
758794
index.cloned(),
759795
rev.map(ToString::to_string),

crates/uv/src/settings.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,7 +1351,7 @@ pub(crate) struct AddSettings {
13511351
pub(crate) package: Option<PackageName>,
13521352
pub(crate) script: Option<PathBuf>,
13531353
pub(crate) python: Option<String>,
1354-
pub(crate) workspace: bool,
1354+
pub(crate) workspace: Option<bool>,
13551355
pub(crate) install_mirrors: PythonInstallMirrors,
13561356
pub(crate) refresh: Refresh,
13571357
pub(crate) indexes: Vec<Index>,
@@ -1390,6 +1390,7 @@ impl AddSettings {
13901390
script,
13911391
python,
13921392
workspace,
1393+
no_workspace,
13931394
} = args;
13941395

13951396
let dependency_type = if let Some(extra) = optional {
@@ -1490,7 +1491,7 @@ impl AddSettings {
14901491
package,
14911492
script,
14921493
python: python.and_then(Maybe::into_option),
1493-
workspace,
1494+
workspace: flag(workspace, no_workspace, "workspace"),
14941495
editable: flag(editable, no_editable, "editable"),
14951496
extras: extra.unwrap_or_default(),
14961497
refresh: Refresh::from(refresh),

0 commit comments

Comments
 (0)