diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 23d5298af..e741a1d6c 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -518,21 +518,25 @@ impl PyProjectTomlMut { Ok(added) } - /// Set the minimum version for an existing dependency in `project.dependencies`. + /// Set the minimum version for an existing dependency. pub fn set_dependency_minimum_version( &mut self, + dependency_type: &DependencyType, index: usize, version: Version, ) -> Result<(), Error> { - // Get or create `project.dependencies`. - let dependencies = self - .project()? - .entry("dependencies") - .or_insert(Item::Value(Value::Array(Array::new()))) - .as_array_mut() - .ok_or(Error::MalformedDependencies)?; + let group = match dependency_type { + DependencyType::Production => self.set_project_dependency_minimum_version()?, + DependencyType::Dev => self.set_dev_dependency_minimum_version()?, + DependencyType::Optional(ref extra) => { + self.set_optional_dependency_minimum_version(extra)? + } + DependencyType::Group(ref group) => { + self.set_dependency_group_requirement_minimum_version(group)? + } + }; - let Some(req) = dependencies.get(index) else { + let Some(req) = group.get(index) else { return Err(Error::MissingDependency(index)); }; @@ -543,17 +547,26 @@ impl PyProjectTomlMut { req.version_or_url = Some(VersionOrUrl::VersionSpecifier(VersionSpecifiers::from( VersionSpecifier::greater_than_equal_version(version), ))); - dependencies.replace(index, req.to_string()); + group.replace(index, req.to_string()); Ok(()) } + /// Set the minimum version for an existing dependency in `project.dependencies`. + fn set_project_dependency_minimum_version(&mut self) -> Result<&mut Array, Error> { + // Get or create `project.dependencies`. + let dependencies = self + .project()? + .entry("dependencies") + .or_insert(Item::Value(Value::Array(Array::new()))) + .as_array_mut() + .ok_or(Error::MalformedDependencies)?; + + Ok(dependencies) + } + /// Set the minimum version for an existing dependency in `tool.uv.dev-dependencies`. - pub fn set_dev_dependency_minimum_version( - &mut self, - index: usize, - version: Version, - ) -> Result<(), Error> { + fn set_dev_dependency_minimum_version(&mut self) -> Result<&mut Array, Error> { // Get or create `tool.uv.dev-dependencies`. let dev_dependencies = self .doc @@ -570,29 +583,14 @@ impl PyProjectTomlMut { .as_array_mut() .ok_or(Error::MalformedDependencies)?; - let Some(req) = dev_dependencies.get(index) else { - return Err(Error::MissingDependency(index)); - }; - - let mut req = req - .as_str() - .and_then(try_parse_requirement) - .ok_or(Error::MalformedDependencies)?; - req.version_or_url = Some(VersionOrUrl::VersionSpecifier(VersionSpecifiers::from( - VersionSpecifier::greater_than_equal_version(version), - ))); - dev_dependencies.replace(index, req.to_string()); - - Ok(()) + Ok(dev_dependencies) } /// Set the minimum version for an existing dependency in `project.optional-dependencies`. - pub fn set_optional_dependency_minimum_version( + fn set_optional_dependency_minimum_version( &mut self, group: &ExtraName, - index: usize, - version: Version, - ) -> Result<(), Error> { + ) -> Result<&mut Array, Error> { // Get or create `project.optional-dependencies`. let optional_dependencies = self .project()? @@ -601,48 +599,30 @@ impl PyProjectTomlMut { .as_table_like_mut() .ok_or(Error::MalformedDependencies)?; - // Try to find the existing group. - let existing_group = optional_dependencies.iter_mut().find_map(|(key, value)| { - if ExtraName::from_str(key.get()).is_ok_and(|g| g == *group) { - Some(value) + // Try to find the existing extra. + let existing_key = optional_dependencies.iter().find_map(|(key, _value)| { + if ExtraName::from_str(key).is_ok_and(|g| g == *group) { + Some(key.to_string()) } else { None } }); // If the group doesn't exist, create it. - let group = match existing_group { - Some(value) => value, - None => optional_dependencies - .entry(group.as_ref()) - .or_insert(Item::Value(Value::Array(Array::new()))), - } - .as_array_mut() - .ok_or(Error::MalformedDependencies)?; - - let Some(req) = group.get(index) else { - return Err(Error::MissingDependency(index)); - }; - - let mut req = req - .as_str() - .and_then(try_parse_requirement) + let group = optional_dependencies + .entry(existing_key.as_deref().unwrap_or(group.as_ref())) + .or_insert(Item::Value(Value::Array(Array::new()))) + .as_array_mut() .ok_or(Error::MalformedDependencies)?; - req.version_or_url = Some(VersionOrUrl::VersionSpecifier(VersionSpecifiers::from( - VersionSpecifier::greater_than_equal_version(version), - ))); - group.replace(index, req.to_string()); - Ok(()) + Ok(group) } /// Set the minimum version for an existing dependency in `dependency-groups`. - pub fn set_dependency_group_requirement_minimum_version( + fn set_dependency_group_requirement_minimum_version( &mut self, group: &GroupName, - index: usize, - version: Version, - ) -> Result<(), Error> { + ) -> Result<&mut Array, Error> { // Get or create `dependency-groups`. let dependency_groups = self .doc @@ -652,38 +632,22 @@ impl PyProjectTomlMut { .ok_or(Error::MalformedDependencies)?; // Try to find the existing group. - let existing_group = dependency_groups.iter_mut().find_map(|(key, value)| { - if GroupName::from_str(key.get()).is_ok_and(|g| g == *group) { - Some(value) + let existing_key = dependency_groups.iter().find_map(|(key, _value)| { + if GroupName::from_str(key).is_ok_and(|g| g == *group) { + Some(key.to_string()) } else { None } }); // If the group doesn't exist, create it. - let group = match existing_group { - Some(value) => value, - None => dependency_groups - .entry(group.as_ref()) - .or_insert(Item::Value(Value::Array(Array::new()))), - } - .as_array_mut() - .ok_or(Error::MalformedDependencies)?; - - let Some(req) = group.get(index) else { - return Err(Error::MissingDependency(index)); - }; - - let mut req = req - .as_str() - .and_then(try_parse_requirement) + let group = dependency_groups + .entry(existing_key.as_deref().unwrap_or(group.as_ref())) + .or_insert(Item::Value(Value::Array(Array::new()))) + .as_array_mut() .ok_or(Error::MalformedDependencies)?; - req.version_or_url = Some(VersionOrUrl::VersionSpecifier(VersionSpecifiers::from( - VersionSpecifier::greater_than_equal_version(version), - ))); - group.replace(index, req.to_string()); - Ok(()) + Ok(group) } /// Adds a source to `tool.uv.sources`. diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 41601b1ed..44b91c29d 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -438,6 +438,126 @@ pub(crate) async fn add( DependencyTarget::PyProjectToml, ), }?; + let edits = edits( + requirements, + &target, + editable, + &dependency_type, + raw_sources, + rev.as_deref(), + tag.as_deref(), + branch.as_deref(), + &extras, + index, + &mut toml, + )?; + + // Add any indexes that were provided on the command-line, in priority order. + if !raw_sources { + let urls = IndexUrls::from_indexes(indexes); + for index in urls.defined_indexes() { + toml.add_index(index)?; + } + } + + let content = toml.to_string(); + + // Save the modified `pyproject.toml` or script. + let 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. + if frozen { + return Ok(ExitStatus::Success); + } + + // If we're modifying a script, and lockfile doesn't exist, don't create it. + if let AddTarget::Script(ref script, _) = target { + if !LockTarget::from(script).lock_path().is_file() { + writeln!( + printer.stderr(), + "Updated `{}`", + script.path.user_display().cyan() + )?; + return Ok(ExitStatus::Success); + } + } + + // Store the content prior to any modifications. + let snapshot = target.snapshot().await?; + + // Update the `pypackage.toml` in-memory. + let target = target.update(&content)?; + + // Set the Ctrl-C handler to revert changes on exit. + let _ = ctrlc::set_handler({ + let snapshot = snapshot.clone(); + move || { + if modified { + let _ = snapshot.revert(); + } + + #[allow(clippy::exit, clippy::cast_possible_wrap)] + std::process::exit(if cfg!(windows) { + 0xC000_013A_u32 as i32 + } else { + 130 + }); + } + }); + + // Use separate state for locking and syncing. + let lock_state = state.fork(); + let sync_state = state; + + match Box::pin(lock_and_sync( + target, + &mut toml, + &edits, + lock_state, + sync_state, + locked, + &dependency_type, + raw_sources, + constraints, + &settings, + &network_settings, + installer_metadata, + concurrency, + cache, + printer, + preview, + )) + .await + { + Ok(()) => Ok(ExitStatus::Success), + Err(err) => { + if modified { + let _ = snapshot.revert(); + } + match err { + ProjectError::Operation(err) => diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls).with_hint(format!("If you want to add the package regardless of the failed resolution, provide the `{}` flag to skip locking and syncing.", "--frozen".green())) + .report(err) + .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())), + err => Err(err.into()), + } + } + } +} + +fn edits( + requirements: Vec, + target: &AddTarget, + editable: Option, + dependency_type: &DependencyType, + raw_sources: bool, + rev: Option<&str>, + tag: Option<&str>, + branch: Option<&str>, + extras: &[ExtraName], + index: Option<&IndexName>, + toml: &mut PyProjectTomlMut, +) -> Result> { let mut edits = Vec::::with_capacity(requirements.len()); for mut requirement in requirements { // Add the specified extras. @@ -461,9 +581,9 @@ pub(crate) async fn add( false, editable, index.cloned(), - rev.clone(), - tag.clone(), - branch.clone(), + rev.map(ToString::to_string), + tag.map(ToString::to_string), + branch.map(ToString::to_string), script_dir, existing_sources, )? @@ -485,9 +605,9 @@ pub(crate) async fn add( workspace, editable, index.cloned(), - rev.clone(), - tag.clone(), - branch.clone(), + rev.map(ToString::to_string), + tag.map(ToString::to_string), + branch.map(ToString::to_string), project.root(), existing_sources, )? @@ -609,98 +729,7 @@ pub(crate) async fn add( edit, }); } - - // Add any indexes that were provided on the command-line, in priority order. - if !raw_sources { - let urls = IndexUrls::from_indexes(indexes); - for index in urls.defined_indexes() { - toml.add_index(index)?; - } - } - - let content = toml.to_string(); - - // Save the modified `pyproject.toml` or script. - let 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. - if frozen { - return Ok(ExitStatus::Success); - } - - // If we're modifying a script, and lockfile doesn't exist, don't create it. - if let AddTarget::Script(ref script, _) = target { - if !LockTarget::from(script).lock_path().is_file() { - writeln!( - printer.stderr(), - "Updated `{}`", - script.path.user_display().cyan() - )?; - return Ok(ExitStatus::Success); - } - } - - // Store the content prior to any modifications. - let snapshot = target.snapshot().await?; - - // Update the `pypackage.toml` in-memory. - let target = target.update(&content)?; - - // Set the Ctrl-C handler to revert changes on exit. - let _ = ctrlc::set_handler({ - let snapshot = snapshot.clone(); - move || { - if modified { - let _ = snapshot.revert(); - } - - #[allow(clippy::exit, clippy::cast_possible_wrap)] - std::process::exit(if cfg!(windows) { - 0xC000_013A_u32 as i32 - } else { - 130 - }); - } - }); - - // Use separate state for locking and syncing. - let lock_state = state.fork(); - let sync_state = state; - - match Box::pin(lock_and_sync( - target, - &mut toml, - &edits, - lock_state, - sync_state, - locked, - &dependency_type, - raw_sources, - constraints, - &settings, - &network_settings, - installer_metadata, - concurrency, - cache, - printer, - preview, - )) - .await - { - Ok(()) => Ok(ExitStatus::Success), - Err(err) => { - if modified { - let _ = snapshot.revert(); - } - match err { - ProjectError::Operation(err) => diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls).with_hint(format!("If you want to add the package regardless of the failed resolution, provide the `{}` flag to skip locking and syncing.", "--frozen".green())) - .report(err) - .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())), - err => Err(err.into()), - } - } - } + Ok(edits) } /// Re-lock and re-sync the project after a series of edits. @@ -801,20 +830,7 @@ async fn lock_and_sync( // For example, convert `1.2.3+local` to `1.2.3`. let minimum = (*minimum).clone().without_local(); - match edit.dependency_type { - DependencyType::Production => { - toml.set_dependency_minimum_version(*index, minimum)?; - } - DependencyType::Dev => { - toml.set_dev_dependency_minimum_version(*index, minimum)?; - } - DependencyType::Optional(ref extra) => { - toml.set_optional_dependency_minimum_version(extra, *index, minimum)?; - } - DependencyType::Group(ref group) => { - toml.set_dependency_group_requirement_minimum_version(group, *index, minimum)?; - } - } + toml.set_dependency_minimum_version(&edit.dependency_type, *index, minimum)?; modified = true; }