Skip to content
Merged
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
10 changes: 9 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3632,7 +3632,8 @@ pub struct AddArgs {
long,
conflicts_with = "dev",
conflicts_with = "optional",
conflicts_with = "package"
conflicts_with = "package",
conflicts_with = "workspace"
)]
pub script: Option<PathBuf>,

Expand All @@ -3648,6 +3649,13 @@ pub struct AddArgs {
value_parser = parse_maybe_string,
)]
pub python: Option<Maybe<String>>,

/// Add the dependency as a workspace member.
///
/// 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)]
pub workspace: bool,
}

#[derive(Args)]
Expand Down
89 changes: 83 additions & 6 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub(crate) async fn add(
extras_of_dependency: Vec<ExtraName>,
package: Option<PackageName>,
python: Option<String>,
workspace: bool,
install_mirrors: PythonInstallMirrors,
settings: ResolverInstallerSettings,
network_settings: NetworkSettings,
Expand Down Expand Up @@ -151,7 +152,7 @@ pub(crate) async fn add(
// Default groups we need the actual project for, interpreter discovery will use this!
let defaulted_groups;

let target = if let Some(script) = script {
let mut target = if let Some(script) = script {
// If we found a PEP 723 script and the user provided a project-only setting, warn.
if package.is_some() {
warn_user_once!(
Expand Down Expand Up @@ -478,6 +479,9 @@ pub(crate) async fn add(
}
}

// Store the content prior to any modifications.
let snapshot = target.snapshot().await?;

// If the user provides a single, named index, pin all requirements to that index.
let index = indexes
.first()
Expand All @@ -488,7 +492,72 @@ pub(crate) async fn add(
debug!("Pinning all requirements to index: `{index}`");
});

// Add the requirements to the `pyproject.toml` or script.
// 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 {
let AddTarget::Project(project, python_target) = target else {
unreachable!("`--workspace` and `--script` are conflicting options");
};

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

// Check each requirement to see if it's a path dependency
for requirement in &requirements {
if let RequirementSource::Directory { install_path, .. } = &requirement.source {
let absolute_path = if install_path.is_absolute() {
install_path.to_path_buf()
} else {
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()
)?;
}
}
}

// If we modified the workspace root, we need to reload it entirely, since this can impact
// the discovered members, etc.
target = if modified {
let workspace_content = toml.to_string();
fs_err::write(
workspace.install_path().join("pyproject.toml"),
&workspace_content,
)?;

AddTarget::Project(
VirtualProject::discover(
project.root(),
&DiscoveryOptions::default(),
&WorkspaceCache::default(),
)
.await?,
python_target,
)
} else {
AddTarget::Project(project, python_target)
}
}

let mut toml = match &target {
AddTarget::Script(script, _) => {
PyProjectTomlMut::from_toml(&script.metadata.raw, DependencyTarget::Script)
Expand All @@ -498,6 +567,7 @@ pub(crate) async fn add(
DependencyTarget::PyProjectToml,
),
}?;

let edits = edits(
requirements,
&target,
Expand Down Expand Up @@ -543,7 +613,7 @@ pub(crate) async fn add(
let content = toml.to_string();

// Save the modified `pyproject.toml` or script.
let modified = target.write(&content)?;
modified |= target.write(&content)?;

// If `--frozen`, exit early. There's no reason to lock and sync, since we don't need a `uv.lock`
// to exist at all.
Expand All @@ -563,9 +633,6 @@ pub(crate) async fn add(
}
}

// Store the content prior to any modifications.
let snapshot = target.snapshot().await?;

// Update the `pypackage.toml` in-memory.
let target = target.update(&content)?;

Expand Down Expand Up @@ -1296,6 +1363,16 @@ impl AddTargetSnapshot {
Ok(())
}
Self::Project(project, lock) => {
// Write the workspace `pyproject.toml` back to disk.
let workspace = project.workspace();
if workspace.install_path() != project.root() {
debug!("Reverting changes to workspace `pyproject.toml`");
fs_err::write(
workspace.install_path().join("pyproject.toml"),
workspace.pyproject_toml().as_ref(),
)?;
}

// Write the `pyproject.toml` back to disk.
debug!("Reverting changes to `pyproject.toml`");
fs_err::write(
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 @@ -1965,6 +1965,7 @@ async fn run_project(
args.extras,
args.package,
args.python,
args.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 @@ -1326,6 +1326,7 @@ pub(crate) struct AddSettings {
pub(crate) package: Option<PackageName>,
pub(crate) script: Option<PathBuf>,
pub(crate) python: Option<String>,
pub(crate) workspace: bool,
pub(crate) install_mirrors: PythonInstallMirrors,
pub(crate) refresh: Refresh,
pub(crate) indexes: Vec<Index>,
Expand Down Expand Up @@ -1363,6 +1364,7 @@ impl AddSettings {
package,
script,
python,
workspace,
} = args;

let dependency_type = if let Some(extra) = optional {
Expand Down Expand Up @@ -1463,6 +1465,7 @@ impl AddSettings {
package,
script,
python: python.and_then(Maybe::into_option),
workspace,
editable: flag(editable, no_editable, "editable"),
extras: extra.unwrap_or_default(),
refresh: Refresh::from(refresh),
Expand Down
Loading
Loading