Skip to content

Commit

Permalink
Add resolver error context to run and tool run (#5991)
Browse files Browse the repository at this point in the history
## Summary

Closes #5530.
  • Loading branch information
charliermarsh authored Aug 10, 2024
1 parent f10c282 commit 2822dde
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 81 deletions.
72 changes: 61 additions & 11 deletions crates/uv-resolver/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,7 @@ pub struct NoSolutionError {
}

impl NoSolutionError {
pub fn header(&self) -> String {
match &self.markers {
ResolverMarkers::Universal { .. } | ResolverMarkers::SpecificEnvironment(_) => {
"No solution found when resolving dependencies:".to_string()
}
ResolverMarkers::Fork(markers) => {
format!("No solution found when resolving dependencies for split ({markers:?}):")
}
}
}

/// Create a new [`NoSolutionError`] from a [`pubgrub::NoSolutionError`].
pub(crate) fn new(
error: pubgrub::NoSolutionError<UvDependencyProvider>,
available_versions: FxHashMap<PubGrubPackage, BTreeSet<Version>>,
Expand Down Expand Up @@ -206,6 +196,11 @@ impl NoSolutionError {
collapse(derivation_tree)
.expect("derivation tree should contain at least one external term")
}

/// Initialize a [`NoSolutionHeader`] for this error.
pub fn header(&self) -> NoSolutionHeader {
NoSolutionHeader::new(self.markers.clone())
}
}

impl std::error::Error for NoSolutionError {}
Expand Down Expand Up @@ -236,3 +231,58 @@ impl std::fmt::Display for NoSolutionError {
Ok(())
}
}

#[derive(Debug)]
pub struct NoSolutionHeader {
/// The [`ResolverMarkers`] that caused the failure.
markers: ResolverMarkers,
/// The additional context for the resolution failure.
context: Option<&'static str>,
}

impl NoSolutionHeader {
/// Create a new [`NoSolutionHeader`] with the given [`ResolverMarkers`].
pub fn new(markers: ResolverMarkers) -> Self {
Self {
markers,
context: None,
}
}

/// Set the context for the resolution failure.
#[must_use]
pub fn with_context(mut self, context: &'static str) -> Self {
self.context = Some(context);
self
}
}

impl std::fmt::Display for NoSolutionHeader {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.markers {
ResolverMarkers::SpecificEnvironment(_) | ResolverMarkers::Universal { .. } => {
if let Some(context) = self.context {
write!(
f,
"No solution found when resolving {context} dependencies:"
)
} else {
write!(f, "No solution found when resolving dependencies:")
}
}
ResolverMarkers::Fork(markers) => {
if let Some(context) = self.context {
write!(
f,
"No solution found when resolving {context} dependencies for split ({markers:?}):",
)
} else {
write!(
f,
"No solution found when resolving dependencies for split ({markers:?}):",
)
}
}
}
}
}
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::{NoSolutionError, ResolveError};
pub use error::{NoSolutionError, NoSolutionHeader, ResolveError};
pub use exclude_newer::ExcludeNewer;
pub use exclusions::Exclusions;
pub use flat_index::FlatIndex;
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ struct DependencyEdit<'a> {
#[diagnostic()]
struct WithHelp {
/// The header to render in the error message.
header: String,
header: uv_resolver::NoSolutionHeader,

/// The underlying error.
#[source]
Expand Down
13 changes: 7 additions & 6 deletions crates/uv/src/commands/project/environment.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
use tracing::debug;

use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::project::{resolve_environment, sync_environment};
use crate::commands::SharedState;
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;
use cache_key::{cache_digest, hash_digest};
use distribution_types::Resolution;
use uv_cache::{Cache, CacheBucket};
Expand All @@ -13,6 +8,12 @@ use uv_configuration::{Concurrency, PreviewMode};
use uv_python::{Interpreter, PythonEnvironment};
use uv_requirements::RequirementsSpecification;

use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::project::{resolve_environment, sync_environment, ProjectError};
use crate::commands::SharedState;
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;

/// A [`PythonEnvironment`] stored in the cache.
#[derive(Debug)]
pub(crate) struct CachedEnvironment(PythonEnvironment);
Expand All @@ -39,7 +40,7 @@ impl CachedEnvironment {
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> anyhow::Result<Self> {
) -> Result<Self, ProjectError> {
// When caching, always use the base interpreter, rather than that of the virtual
// environment.
let interpreter = if let Some(interpreter) = interpreter.to_base_interpreter(cache)? {
Expand Down
18 changes: 12 additions & 6 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,25 @@ pub(crate) enum ProjectError {
Lock(#[from] uv_resolver::LockError),

#[error(transparent)]
Fmt(#[from] std::fmt::Error),
Operation(#[from] pip::operations::Error),

#[error(transparent)]
Io(#[from] std::io::Error),
RequiresPython(#[from] uv_resolver::RequiresPythonError),

#[error(transparent)]
Anyhow(#[from] anyhow::Error),
Interpreter(#[from] uv_python::InterpreterError),

#[error(transparent)]
Operation(#[from] pip::operations::Error),
Tool(#[from] uv_tool::Error),

#[error(transparent)]
RequiresPython(#[from] uv_resolver::RequiresPythonError),
Fmt(#[from] std::fmt::Error),

#[error(transparent)]
Io(#[from] std::io::Error),

#[error(transparent)]
Anyhow(#[from] anyhow::Error),
}

/// Compute the `Requires-Python` bound for the [`Workspace`].
Expand Down Expand Up @@ -500,7 +506,7 @@ pub(crate) async fn resolve_environment<'a>(
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> anyhow::Result<ResolutionGraph> {
) -> Result<ResolutionGraph, ProjectError> {
warn_on_requirements_txt_setting(&spec, settings);

let ResolverSettingsRef {
Expand Down
51 changes: 41 additions & 10 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use std::ffi::OsString;
use std::fmt::Write;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, bail, Context, Result};
use anstream::eprint;
use anyhow::{anyhow, bail, Context};
use itertools::Itertools;
use owo_colors::OwoColorize;
use tokio::process::Command;
Expand All @@ -23,13 +24,14 @@ use uv_python::{
PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, VersionRequest,
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_scripts::Pep723Script;
use uv_scripts::{Pep723Error, Pep723Script};
use uv_warnings::warn_user_once;
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceError};

use crate::commands::pip::loggers::{
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
};
use crate::commands::pip::operations;
use crate::commands::pip::operations::Modifications;
use crate::commands::project::environment::CachedEnvironment;
use crate::commands::project::{ProjectError, WorkspacePython};
Expand Down Expand Up @@ -62,7 +64,7 @@ pub(crate) async fn run(
native_tls: bool,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
) -> anyhow::Result<ExitStatus> {
if preview.is_disabled() {
warn_user_once!("`uv run` is experimental and may change without warning");
}
Expand Down Expand Up @@ -162,7 +164,7 @@ pub(crate) async fn run(
})
.collect::<Result<_, _>>()?;
let spec = RequirementsSpecification::from_requirements(requirements);
let environment = CachedEnvironment::get_or_create(
let result = CachedEnvironment::get_or_create(
spec,
interpreter,
&settings,
Expand All @@ -184,7 +186,20 @@ pub(crate) async fn run(
cache,
printer,
)
.await?;
.await;

let environment = match result {
Ok(resolution) => resolution,
Err(ProjectError::Operation(operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}"))
.context(err.header().with_context("script"));
eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
};

Some(environment.into_interpreter())
} else {
Expand Down Expand Up @@ -515,7 +530,7 @@ pub(crate) async fn run(
Some(spec) => {
debug!("Syncing ephemeral requirements");

CachedEnvironment::get_or_create(
let result = CachedEnvironment::get_or_create(
spec,
base_interpreter.clone(),
&settings,
Expand All @@ -537,8 +552,22 @@ pub(crate) async fn run(
cache,
printer,
)
.await?
.into()
.await;

let environment = match result {
Ok(resolution) => resolution,
Err(ProjectError::Operation(operations::Error::Resolve(
uv_resolver::ResolveError::NoSolution(err),
))) => {
let report = miette::Report::msg(format!("{err}"))
.context(err.header().with_context("`--with`"));
eprint!("{report:?}");
return Ok(ExitStatus::Failure);
}
Err(err) => return Err(err.into()),
};

environment.into()
}
})
};
Expand Down Expand Up @@ -612,7 +641,9 @@ pub(crate) async fn run(
}

/// Read a [`Pep723Script`] from the given command.
pub(crate) async fn parse_script(command: &ExternalCommand) -> Result<Option<Pep723Script>> {
pub(crate) async fn parse_script(
command: &ExternalCommand,
) -> Result<Option<Pep723Script>, Pep723Error> {
// Parse the input command.
let command = RunCommand::from(command);

Expand All @@ -621,7 +652,7 @@ pub(crate) async fn parse_script(command: &ExternalCommand) -> Result<Option<Pep
};

// Read the PEP 723 `script` metadata from the target script.
Ok(Pep723Script::read(&target).await?)
Pep723Script::read(&target).await
}

/// Returns `true` if we can skip creating an additional ephemeral environment in `uv run`.
Expand Down
Loading

0 comments on commit 2822dde

Please sign in to comment.