Skip to content

Commit

Permalink
Avoid persisting uv add calls that result in resolver errors (#5664)
Browse files Browse the repository at this point in the history
## Summary

Closes #5622.
  • Loading branch information
charliermarsh committed Jul 31, 2024
1 parent 54398fa commit 4b8a127
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 5 deletions.
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub use dependency_mode::DependencyMode;
pub use error::ResolveError;
pub use error::{NoSolutionError, ResolveError};
pub use exclude_newer::ExcludeNewer;
pub use exclusions::Exclusions;
pub use flat_index::FlatIndex;
Expand Down
6 changes: 6 additions & 0 deletions crates/uv-workspace/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ impl PartialEq for PyProjectToml {

impl Eq for PyProjectToml {}

impl AsRef<[u8]> for PyProjectToml {
fn as_ref(&self) -> &[u8] {
self.raw.as_bytes()
}
}

/// PEP 621 project metadata (`project`).
///
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
Expand Down
46 changes: 42 additions & 4 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ use uv_workspace::{DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace

use crate::commands::pip::operations::Modifications;
use crate::commands::pip::resolution_environment;
use crate::commands::project::ProjectError;
use crate::commands::reporters::ResolverReporter;
use crate::commands::{project, ExitStatus, SharedState};
use crate::commands::{pip, project, ExitStatus, SharedState};
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;

Expand Down Expand Up @@ -154,7 +155,8 @@ pub(crate) async fn add(
.await?;

// Add the requirements to the `pyproject.toml`.
let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
let existing = project.current_project().pyproject_toml();
let mut pyproject = PyProjectTomlMut::from_toml(existing)?;
for mut req in requirements {
// Add the specified extras.
req.extras.extend(extras.iter().cloned());
Expand Down Expand Up @@ -221,7 +223,7 @@ pub(crate) async fn add(
let state = SharedState::default();

// Lock and sync the environment, if necessary.
let lock = project::lock::do_safe_lock(
let lock = match project::lock::do_safe_lock(
locked,
frozen,
project.workspace(),
Expand All @@ -235,7 +237,26 @@ pub(crate) async fn add(
cache,
printer,
)
.await?;
.await
{
Ok(lock) => lock,
Err(ProjectError::Operation(pip::operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let header = err.header();
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:?}");

// Revert the changes to the `pyproject.toml`.
fs_err::write(
project.current_project().root().join("pyproject.toml"),
existing,
)?;

return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
};

// Perform a full sync, because we don't know what exactly is affected by the removal.
// TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here?
Expand All @@ -262,3 +283,20 @@ pub(crate) async fn add(

Ok(ExitStatus::Success)
}

/// Render a [`uv_resolver::NoSolutionError`] with a help message.
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("{header}")]
#[diagnostic()]
struct WithHelp {
/// The header to render in the error message.
header: String,

/// The underlying error.
#[source]
cause: uv_resolver::NoSolutionError,

/// The help message to display.
#[help]
help: Option<&'static str>,
}
43 changes: 43 additions & 0 deletions crates/uv/tests/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2101,3 +2101,46 @@ fn add_reject_multiple_git_ref_flags() {
"###
);
}

/// Avoiding persisting `add` calls when resolution fails.
#[test]
fn add_error() -> 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(&["xyz"]), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: `uv add` is experimental and may change without warning
× No solution found when resolving dependencies:
╰─▶ Because there are no versions of xyz and project==0.1.0 depends on xyz, we can conclude that project==0.1.0 cannot be used.
And because only project==0.1.0 is available and you require project, we can conclude that the requirements are unsatisfiable.
help: If this is intentional, run `uv add --frozen` to skip the lock and sync steps.
"###);

uv_snapshot!(context.filters(), context.add(&["xyz"]).arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv add` is experimental and may change without warning
"###);

let lock = context.temp_dir.join("uv.lock");
assert!(!lock.exists());

Ok(())
}

0 comments on commit 4b8a127

Please sign in to comment.