Make repeated uv add operations simpler (#5922)

## Summary

Closes #5913.
This commit is contained in:
Charlie Marsh 2024-08-08 13:55:07 -04:00 committed by GitHub
parent 2789830ac2
commit dc3f498f58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 102 additions and 13 deletions

View file

@ -30,7 +30,7 @@ pub struct PyProjectToml {
pub tool: Option<Tool>, pub tool: Option<Tool>,
/// The raw unserialized document. /// The raw unserialized document.
#[serde(skip)] #[serde(skip)]
pub(crate) raw: String, pub raw: String,
} }
impl PyProjectToml { impl PyProjectToml {

View file

@ -55,11 +55,6 @@ impl PyProjectTomlMut {
}) })
} }
/// Initialize a [`PyProjectToml`] from a [`PyProjectTomlMut`].
pub fn to_toml(&self) -> Result<PyProjectToml, Error> {
Ok(toml::from_str(&self.doc.to_string()).map_err(Box::new)?)
}
/// Adds a project to the workspace. /// Adds a project to the workspace.
pub fn add_workspace(&mut self, path: impl AsRef<Path>) -> Result<(), Error> { pub fn add_workspace(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
// Get or create `tool.uv.workspace.members`. // Get or create `tool.uv.workspace.members`.

View file

@ -2,9 +2,9 @@ use std::collections::hash_map::Entry;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use rustc_hash::{FxBuildHasher, FxHashMap};
use pep508_rs::{ExtraName, Requirement, VersionOrUrl}; use pep508_rs::{ExtraName, Requirement, VersionOrUrl};
use rustc_hash::{FxBuildHasher, FxHashMap};
use tracing::debug;
use uv_auth::store_credentials_from_url; use uv_auth::store_credentials_from_url;
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
@ -247,7 +247,14 @@ pub(crate) async fn add(
} }
// Save the modified `pyproject.toml`. // Save the modified `pyproject.toml`.
fs_err::write(project.root().join("pyproject.toml"), pyproject.to_string())?; let mut modified = false;
let content = pyproject.to_string();
if content == existing.raw {
debug!("No changes to `pyproject.toml`; skipping update");
} else {
fs_err::write(project.root().join("pyproject.toml"), &content)?;
modified = true;
}
// If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock` // If `--frozen`, exit early. There's no reason to lock and sync, and we don't need a `uv.lock`
// to exist at all. // to exist at all.
@ -258,7 +265,7 @@ pub(crate) async fn add(
// Update the `pypackage.toml` in-memory. // Update the `pypackage.toml` in-memory.
let project = project let project = project
.clone() .clone()
.with_pyproject_toml(pyproject.to_toml()?) .with_pyproject_toml(toml::from_str(&content)?)
.context("Failed to update `pyproject.toml`")?; .context("Failed to update `pyproject.toml`")?;
// Lock and sync the environment, if necessary. // Lock and sync the environment, if necessary.
@ -286,8 +293,10 @@ pub(crate) async fn add(
let report = miette::Report::new(WithHelp { header, cause: err, help: Some("If this is intentional, run `uv add --frozen` to skip the lock and sync steps.") }); let report = miette::Report::new(WithHelp { header, cause: err, help: Some("If this is intentional, run `uv add --frozen` to skip the lock and sync steps.") });
anstream::eprint!("{report:?}"); anstream::eprint!("{report:?}");
// Revert the changes to the `pyproject.toml`. // Revert the changes to the `pyproject.toml`, if necessary.
fs_err::write(project.root().join("pyproject.toml"), existing)?; if modified {
fs_err::write(project.root().join("pyproject.toml"), existing)?;
}
return Ok(ExitStatus::Failure); return Ok(ExitStatus::Failure);
} }
@ -361,7 +370,9 @@ pub(crate) async fn add(
modified = true; modified = true;
} }
// Save the modified `pyproject.toml`. // Save the modified `pyproject.toml`. No need to check for changes in the underlying
// string content, since the above loop _must_ change an empty specifier to a non-empty
// specifier.
if modified { if modified {
fs_err::write(project.root().join("pyproject.toml"), pyproject.to_string())?; fs_err::write(project.root().join("pyproject.toml"), pyproject.to_string())?;
} }

View file

@ -2829,3 +2829,86 @@ fn add_virtual() -> Result<()> {
Ok(()) Ok(())
} }
/// Add the same requirement multiple times.
#[test]
fn add_repeat() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
# ...
requires-python = ">=3.12"
dependencies = []
"#})?;
uv_snapshot!(context.filters(), context.add(&["anyio"]), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv add` is experimental and may change without warning
Resolved 4 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r###"
[project]
name = "project"
version = "0.1.0"
# ...
requires-python = ">=3.12"
dependencies = [
"anyio>=4.3.0",
]
"###
);
});
uv_snapshot!(context.filters(), context.add(&["anyio"]), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv add` is experimental and may change without warning
Resolved 4 packages in [TIME]
Audited 4 packages in [TIME]
"###);
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r###"
[project]
name = "project"
version = "0.1.0"
# ...
requires-python = ">=3.12"
dependencies = [
"anyio>=4.3.0",
]
"###
);
});
Ok(())
}