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: 10 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,9 @@ pub enum ProjectCommand {
after_long_help = ""
)]
Lock(LockArgs),
/// Upgrade a dependency in the project.
#[command(hide = true)]
Upgrade(UpgradeArgs),
/// Export the project's lockfile to an alternate format.
///
/// At present, `requirements.txt`, `pylock.toml` (PEP 751) and CycloneDX v1.5 JSON output
Expand Down Expand Up @@ -4269,6 +4272,13 @@ pub struct LockArgs {
pub python: Option<Maybe<String>>,
}

#[derive(Args)]
pub struct UpgradeArgs {
/// The package to upgrade.
#[arg(value_hint = ValueHint::Other)]
pub package: PackageName,
}

#[derive(Args)]
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
pub struct AddArgs {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-distribution/src/metadata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl Metadata {

/// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory
/// dependencies.
pub(crate) async fn from_workspace(
pub async fn from_workspace(
metadata: ResolutionMetadata,
install_path: &Path,
git_source: Option<&GitWorkspaceMember<'_>>,
Expand Down
8 changes: 8 additions & 0 deletions crates/uv-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,14 @@ impl TestContext {
command
}

/// Create a `uv upgrade` command with options shared across scenarios.
pub fn upgrade(&self) -> Command {
let mut command = self.new_command();
command.arg("upgrade");
self.add_shared_options(&mut command, false);
command
}

/// Create a `uv audit` command with options shared across scenarios.
pub fn audit(&self) -> Command {
let mut command = self.new_command();
Expand Down
80 changes: 79 additions & 1 deletion crates/uv-workspace/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,50 @@ impl PyProjectTomlMut {
Ok(edit)
}

/// Replaces a dependency in `project.dependencies` without modifying its source.
///
/// Returns `Some` if the dependency was replaced, or `None` if it was not found.
pub fn replace_dependency(
&mut self,
req: &Requirement,
raw: bool,
) -> Result<Option<ArrayEdit>, Error> {
let Some(dependencies) = self
.project_mut()?
.and_then(|project| project.get_mut("dependencies"))
.map(|dependencies| {
dependencies
.as_array_mut()
.ok_or(Error::MalformedDependencies)
})
.transpose()?
else {
return Ok(None);
};
let mut to_replace = find_dependencies(&req.name, Some(&req.marker), dependencies);

match to_replace.as_slice() {
[] => Ok(None),
[_] => {
let (index, _) = to_replace.remove(0);
let req_string = if raw {
req.displayable_with_credentials().to_string()
} else {
req.to_string()
};
dependencies.replace(index, req_string);
Ok(Some(ArrayEdit::Update(index)))
}
_ => Err(Error::Ambiguous {
package_name: req.name.clone(),
requirements: to_replace
.into_iter()
.map(|(_, requirement)| requirement)
.collect(),
}),
}
}

/// Adds a development dependency to `tool.uv.dev-dependencies`.
///
/// Returns `true` if the dependency was added, `false` if it was updated.
Expand Down Expand Up @@ -1782,12 +1826,17 @@ fn split_specifiers(req: &str) -> (&str, &str) {

#[cfg(test)]
mod test {
use super::{AddBoundsKind, reformat_array_multiline, remove_dependency, split_specifiers};
use super::{
AddBoundsKind, DependencyTarget, PyProjectTomlMut, reformat_array_multiline,
remove_dependency, split_specifiers,
};
use anyhow::Result;
use insta::assert_snapshot;
use std::str::FromStr;
use toml_edit::DocumentMut;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_pep508::Requirement;

#[test]
fn split() {
Expand Down Expand Up @@ -1962,6 +2011,35 @@ dependencies = [
}
}

#[test]
fn replace_dependency_preserves_source() -> Result<()> {
let mut pyproject = PyProjectTomlMut::from_toml(
r#"[project]
dependencies = ["anyio<=2"]

[tool.uv.sources]
anyio = { index = "internal" }
"#,
DependencyTarget::PyProjectToml,
)?;
let requirement = Requirement::from_str("anyio")?;

let replaced = pyproject.replace_dependency(&requirement, false)?;
assert!(replaced.is_some());

assert_snapshot!(
pyproject.to_string(),
@r#"
[project]
dependencies = ["anyio"]

[tool.uv.sources]
anyio = { index = "internal" }
"#
);
Ok(())
}

#[test]
fn remove_preserves_end_of_line_comment_on_previous_item() {
let toml = r#"
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub(crate) use project::remove::remove;
pub(crate) use project::run::{ParsedRunCommand, RunCommand, run};
pub(crate) use project::sync::sync;
pub(crate) use project::tree::tree;
pub(crate) use project::upgrade::upgrade;
pub(crate) use project::version::{project_version, self_version};
pub(crate) use publish::publish;
pub(crate) use python::dir::dir as python_dir;
Expand Down
14 changes: 11 additions & 3 deletions crates/uv/src/commands/project/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1445,7 +1445,7 @@ impl ValidatedLock {
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct LockEventVersion<'lock> {
pub(super) struct LockEventVersion<'lock> {
/// The version of the package, or `None` if the package has a dynamic version.
version: Option<&'lock Version>,
/// The short Git SHA of the package, if it was installed from a Git repository.
Expand Down Expand Up @@ -1474,7 +1474,7 @@ impl std::fmt::Display for LockEventVersion<'_> {

/// A modification to a lockfile.
#[derive(Debug, Clone)]
enum LockEvent<'lock> {
pub(super) enum LockEvent<'lock> {
Update(
DryRun,
PackageName,
Expand All @@ -1487,7 +1487,7 @@ enum LockEvent<'lock> {

impl<'lock> LockEvent<'lock> {
/// Detect the change events between an (optional) existing and updated lockfile.
fn detect_changes(
pub(super) fn detect_changes(
existing_lock: Option<&'lock Lock>,
new_lock: &'lock Lock,
dry_run: DryRun,
Expand Down Expand Up @@ -1548,6 +1548,14 @@ impl<'lock> LockEvent<'lock> {
}
})
}

pub(super) fn package(&self) -> &PackageName {
match self {
Self::Update(_, package, ..)
| Self::Add(_, package, ..)
| Self::Remove(_, package, ..) => package,
}
}
}

impl std::fmt::Display for LockEvent<'_> {
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub(crate) mod remove;
pub(crate) mod run;
pub(crate) mod sync;
pub(crate) mod tree;
pub(crate) mod upgrade;
pub(crate) mod version;

/// The source of a missing lockfile error.
Expand Down
Loading
Loading