mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Include version constraints in derivation chains (#9112)
## Summary Derivation chains can now include the versions at which a package was requested.
This commit is contained in:
parent
a3a543d4f2
commit
8dd095cab8
13 changed files with 235 additions and 198 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4479,6 +4479,7 @@ dependencies = [
|
|||
"uv-virtualenv",
|
||||
"uv-warnings",
|
||||
"uv-workspace",
|
||||
"version-ranges",
|
||||
"walkdir",
|
||||
"which",
|
||||
"zip",
|
||||
|
@ -4925,6 +4926,7 @@ dependencies = [
|
|||
"uv-pep508",
|
||||
"uv-platform-tags",
|
||||
"uv-pypi-types",
|
||||
"version-ranges",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -43,3 +43,4 @@ thiserror = { workspace = true }
|
|||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
version-ranges = { workspace = true }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
use version_ranges::Ranges;
|
||||
|
||||
/// A chain of derivation steps from the root package to the current package, to explain why a
|
||||
/// package is included in the resolution.
|
||||
|
@ -29,18 +30,6 @@ impl DerivationChain {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DerivationChain {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for (idx, step) in self.0.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
write!(f, " -> ")?;
|
||||
}
|
||||
write!(f, "{}=={}", step.name, step.version)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'chain> IntoIterator for &'chain DerivationChain {
|
||||
type Item = &'chain DerivationStep;
|
||||
type IntoIter = std::slice::Iter<'chain, DerivationStep>;
|
||||
|
@ -63,20 +52,20 @@ impl IntoIterator for DerivationChain {
|
|||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct DerivationStep {
|
||||
/// The name of the package.
|
||||
name: PackageName,
|
||||
pub name: PackageName,
|
||||
/// The version of the package.
|
||||
version: Version,
|
||||
pub version: Version,
|
||||
/// The constraints applied to the subsequent package in the chain.
|
||||
pub range: Ranges<Version>,
|
||||
}
|
||||
|
||||
impl DerivationStep {
|
||||
/// Create a [`DerivationStep`] from a package name and version.
|
||||
pub fn new(name: PackageName, version: Version) -> Self {
|
||||
Self { name, version }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DerivationStep {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}=={}", self.name, self.version)
|
||||
pub fn new(name: PackageName, version: Version, range: Ranges<Version>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
version,
|
||||
range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -257,117 +257,15 @@ impl NoSolutionError {
|
|||
/// implement PEP 440 semantics for local version equality. For example, `1.0.0+foo` needs to
|
||||
/// satisfy `==1.0.0`.
|
||||
pub(crate) fn collapse_local_version_segments(derivation_tree: ErrorTree) -> ErrorTree {
|
||||
/// Remove local versions sentinels (`+[max]`) from the interval.
|
||||
fn strip_sentinel(
|
||||
mut lower: Bound<Version>,
|
||||
mut upper: Bound<Version>,
|
||||
) -> (Bound<Version>, Bound<Version>) {
|
||||
match (&lower, &upper) {
|
||||
(Bound::Unbounded, Bound::Unbounded) => {}
|
||||
(Bound::Unbounded, Bound::Included(v)) => {
|
||||
// `<=1.0.0+[max]` is equivalent to `<=1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Included(v.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Unbounded, Bound::Excluded(v)) => {
|
||||
// `<1.0.0+[max]` is equivalent to `<1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Included(v), Bound::Unbounded) => {
|
||||
// `>=1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Included(v), Bound::Included(b)) => {
|
||||
// `>=1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
// `<=1.0.0+[max]` is equivalent to `<=1.0.0`
|
||||
if b.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Included(b.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Included(v), Bound::Excluded(b)) => {
|
||||
// `>=1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
// `<1.0.0+[max]` is equivalent to `<1.0.0`
|
||||
if b.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Included(b.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Excluded(v), Bound::Unbounded) => {
|
||||
// `>1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Excluded(v), Bound::Included(b)) => {
|
||||
// `>1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
// `<=1.0.0+[max]` is equivalent to `<=1.0.0`
|
||||
if b.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Included(b.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Excluded(v), Bound::Excluded(b)) => {
|
||||
// `>1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
// `<1.0.0+[max]` is equivalent to `<1.0.0`
|
||||
if b.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Excluded(b.clone().without_local());
|
||||
}
|
||||
}
|
||||
}
|
||||
(lower, upper)
|
||||
}
|
||||
|
||||
/// Remove local versions sentinels (`+[max]`) from the version ranges.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn strip_sentinels(versions: Ranges<Version>) -> Ranges<Version> {
|
||||
let mut range = Ranges::empty();
|
||||
for (lower, upper) in versions.iter() {
|
||||
let (lower, upper) = strip_sentinel(lower.clone(), upper.clone());
|
||||
range = range.union(&Range::from_range_bounds((lower, upper)));
|
||||
}
|
||||
range
|
||||
}
|
||||
|
||||
/// Returns `true` if the range appears to be, e.g., `>1.0.0, <1.0.0+[max]`.
|
||||
fn is_sentinel(versions: &Ranges<Version>) -> bool {
|
||||
versions.iter().all(|(lower, upper)| {
|
||||
let (Bound::Excluded(lower), Bound::Excluded(upper)) = (lower, upper) else {
|
||||
return false;
|
||||
};
|
||||
if lower.local() == LocalVersionSlice::Max {
|
||||
return false;
|
||||
}
|
||||
if upper.local() != LocalVersionSlice::Max {
|
||||
return false;
|
||||
}
|
||||
*lower == upper.clone().without_local()
|
||||
})
|
||||
}
|
||||
|
||||
fn strip(derivation_tree: ErrorTree) -> Option<ErrorTree> {
|
||||
match derivation_tree {
|
||||
DerivationTree::External(External::NotRoot(_, _)) => Some(derivation_tree),
|
||||
DerivationTree::External(External::NoVersions(package, versions)) => {
|
||||
if is_sentinel(&versions) {
|
||||
if SentinelRange::from(&versions).is_sentinel() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let versions = strip_sentinels(versions);
|
||||
let versions = SentinelRange::from(&versions).strip();
|
||||
Some(DerivationTree::External(External::NoVersions(
|
||||
package, versions,
|
||||
)))
|
||||
|
@ -378,14 +276,14 @@ impl NoSolutionError {
|
|||
package2,
|
||||
versions2,
|
||||
)) => {
|
||||
let versions1 = strip_sentinels(versions1);
|
||||
let versions2 = strip_sentinels(versions2);
|
||||
let versions1 = SentinelRange::from(&versions1).strip();
|
||||
let versions2 = SentinelRange::from(&versions2).strip();
|
||||
Some(DerivationTree::External(External::FromDependencyOf(
|
||||
package1, versions1, package2, versions2,
|
||||
)))
|
||||
}
|
||||
DerivationTree::External(External::Custom(package, versions, reason)) => {
|
||||
let versions = strip_sentinels(versions);
|
||||
let versions = SentinelRange::from(&versions).strip();
|
||||
Some(DerivationTree::External(External::Custom(
|
||||
package, versions, reason,
|
||||
)))
|
||||
|
@ -402,10 +300,10 @@ impl NoSolutionError {
|
|||
.map(|(pkg, term)| {
|
||||
let term = match term {
|
||||
Term::Positive(versions) => {
|
||||
Term::Positive(strip_sentinels(versions))
|
||||
Term::Positive(SentinelRange::from(&versions).strip())
|
||||
}
|
||||
Term::Negative(versions) => {
|
||||
Term::Negative(strip_sentinels(versions))
|
||||
Term::Negative(SentinelRange::from(&versions).strip())
|
||||
}
|
||||
};
|
||||
(pkg, term)
|
||||
|
@ -900,6 +798,119 @@ fn drop_root_dependency_on_project(
|
|||
}
|
||||
}
|
||||
|
||||
/// A version range that may include local version sentinels (`+[max]`).
|
||||
#[derive(Debug)]
|
||||
pub struct SentinelRange<'range>(&'range Range<Version>);
|
||||
|
||||
impl<'range> From<&'range Range<Version>> for SentinelRange<'range> {
|
||||
fn from(range: &'range Range<Version>) -> Self {
|
||||
Self(range)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'range> SentinelRange<'range> {
|
||||
/// Returns `true` if the range appears to be, e.g., `>1.0.0, <1.0.0+[max]`.
|
||||
pub fn is_sentinel(&self) -> bool {
|
||||
self.0.iter().all(|(lower, upper)| {
|
||||
let (Bound::Excluded(lower), Bound::Excluded(upper)) = (lower, upper) else {
|
||||
return false;
|
||||
};
|
||||
if lower.local() == LocalVersionSlice::Max {
|
||||
return false;
|
||||
}
|
||||
if upper.local() != LocalVersionSlice::Max {
|
||||
return false;
|
||||
}
|
||||
*lower == upper.clone().without_local()
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove local versions sentinels (`+[max]`) from the version ranges.
|
||||
pub fn strip(&self) -> Ranges<Version> {
|
||||
let mut range = Ranges::empty();
|
||||
for (lower, upper) in self.0.iter() {
|
||||
let (lower, upper) = Self::strip_sentinel(lower.clone(), upper.clone());
|
||||
range = range.union(&Range::from_range_bounds((lower, upper)));
|
||||
}
|
||||
range
|
||||
}
|
||||
|
||||
/// Remove local versions sentinels (`+[max]`) from the interval.
|
||||
fn strip_sentinel(
|
||||
mut lower: Bound<Version>,
|
||||
mut upper: Bound<Version>,
|
||||
) -> (Bound<Version>, Bound<Version>) {
|
||||
match (&lower, &upper) {
|
||||
(Bound::Unbounded, Bound::Unbounded) => {}
|
||||
(Bound::Unbounded, Bound::Included(v)) => {
|
||||
// `<=1.0.0+[max]` is equivalent to `<=1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Included(v.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Unbounded, Bound::Excluded(v)) => {
|
||||
// `<1.0.0+[max]` is equivalent to `<1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Included(v), Bound::Unbounded) => {
|
||||
// `>=1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Included(v), Bound::Included(b)) => {
|
||||
// `>=1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
// `<=1.0.0+[max]` is equivalent to `<=1.0.0`
|
||||
if b.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Included(b.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Included(v), Bound::Excluded(b)) => {
|
||||
// `>=1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
// `<1.0.0+[max]` is equivalent to `<1.0.0`
|
||||
if b.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Included(b.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Excluded(v), Bound::Unbounded) => {
|
||||
// `>1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Excluded(v), Bound::Included(b)) => {
|
||||
// `>1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
// `<=1.0.0+[max]` is equivalent to `<=1.0.0`
|
||||
if b.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Included(b.clone().without_local());
|
||||
}
|
||||
}
|
||||
(Bound::Excluded(v), Bound::Excluded(b)) => {
|
||||
// `>1.0.0+[max]` is equivalent to `>1.0.0`
|
||||
if v.local() == LocalVersionSlice::Max {
|
||||
lower = Bound::Excluded(v.clone().without_local());
|
||||
}
|
||||
// `<1.0.0+[max]` is equivalent to `<1.0.0`
|
||||
if b.local() == LocalVersionSlice::Max {
|
||||
upper = Bound::Excluded(b.clone().without_local());
|
||||
}
|
||||
}
|
||||
}
|
||||
(lower, upper)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NoSolutionHeader {
|
||||
/// The [`ResolverEnvironment`] that caused the failure.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
pub use dependency_mode::DependencyMode;
|
||||
pub use error::{NoSolutionError, NoSolutionHeader, ResolveError};
|
||||
pub use error::{NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange};
|
||||
pub use exclude_newer::ExcludeNewer;
|
||||
pub use exclusions::Exclusions;
|
||||
pub use flat_index::{FlatDistributions, FlatIndex};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use petgraph::Direction;
|
||||
use pubgrub::{Kind, SelectedDependencies, State};
|
||||
use pubgrub::{Kind, Range, SelectedDependencies, State};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use uv_distribution_types::{
|
||||
|
@ -58,6 +58,7 @@ impl DerivationChainBuilder {
|
|||
path.push(DerivationStep::new(
|
||||
dist.name().clone(),
|
||||
dist.version().clone(),
|
||||
Range::empty(),
|
||||
));
|
||||
for neighbor in resolution
|
||||
.graph()
|
||||
|
@ -88,20 +89,29 @@ impl DerivationChainBuilder {
|
|||
solution: &SelectedDependencies<UvDependencyProvider>,
|
||||
path: &mut Vec<DerivationStep>,
|
||||
) -> bool {
|
||||
// Retrieve the incompatiblies for the current package.
|
||||
let Some(incompats) = state.incompatibilities.get(package) else {
|
||||
// Retrieve the incompatibilities for the current package.
|
||||
let Some(incompatibilities) = state.incompatibilities.get(package) else {
|
||||
return false;
|
||||
};
|
||||
for index in incompats {
|
||||
for index in incompatibilities {
|
||||
let incompat = &state.incompatibility_store[*index];
|
||||
|
||||
// Find a dependency from a package to the current package.
|
||||
if let Kind::FromDependencyOf(p1, _v1, p2, v2) = &incompat.kind {
|
||||
if let Kind::FromDependencyOf(p1, _, p2, v2) = &incompat.kind {
|
||||
if p2 == package && v2.contains(version) {
|
||||
if let Some(version) = solution.get(p1) {
|
||||
if let Some(name) = p1.name_no_root() {
|
||||
if p1.name_no_root() == p2.name_no_root() {
|
||||
// Skip proxied dependencies.
|
||||
if find_path(p1, version, state, solution, path) {
|
||||
return true;
|
||||
}
|
||||
} else if let Some(name) = p1.name_no_root() {
|
||||
// Add to the current path.
|
||||
path.push(DerivationStep::new(name.clone(), version.clone()));
|
||||
path.push(DerivationStep::new(
|
||||
name.clone(),
|
||||
version.clone(),
|
||||
v2.clone(),
|
||||
));
|
||||
|
||||
// Recursively search the next package.
|
||||
if find_path(p1, version, state, solution, path) {
|
||||
|
@ -128,7 +138,6 @@ impl DerivationChainBuilder {
|
|||
return None;
|
||||
}
|
||||
path.reverse();
|
||||
path.dedup();
|
||||
path
|
||||
};
|
||||
|
||||
|
|
|
@ -94,6 +94,7 @@ tracing-subscriber = { workspace = true, features = ["json"] }
|
|||
tracing-tree = { workspace = true }
|
||||
unicode-width = { workspace = true }
|
||||
url = { workspace = true }
|
||||
version-ranges = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
which = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
|
|
@ -3,9 +3,12 @@ use std::sync::LazyLock;
|
|||
|
||||
use owo_colors::OwoColorize;
|
||||
use rustc_hash::FxHashMap;
|
||||
use version_ranges::Ranges;
|
||||
|
||||
use uv_distribution_types::{BuiltDist, DerivationChain, Name, SourceDist};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
use uv_resolver::SentinelRange;
|
||||
|
||||
use crate::commands::pip;
|
||||
|
||||
|
@ -155,6 +158,54 @@ impl OperationDiagnostic {
|
|||
}
|
||||
}
|
||||
|
||||
/// Format a [`DerivationChain`] as a human-readable error message.
|
||||
fn format_chain(name: &PackageName, version: Option<&Version>, chain: &DerivationChain) -> String {
|
||||
let mut message = if let Some(version) = version {
|
||||
format!(
|
||||
"`{}` (v{}) was included because",
|
||||
name.cyan(),
|
||||
version.cyan()
|
||||
)
|
||||
} else {
|
||||
format!("`{}` was included because", name.cyan())
|
||||
};
|
||||
let mut range: Option<Ranges<Version>> = None;
|
||||
for (i, step) in chain.iter().enumerate() {
|
||||
if i > 0 {
|
||||
if let Some(range) =
|
||||
range.filter(|range| *range != Ranges::empty() && *range != Ranges::full())
|
||||
{
|
||||
message = format!(
|
||||
"{message} `{}{}` (v{}) which depends on",
|
||||
step.name.cyan(),
|
||||
range.cyan(),
|
||||
step.version.cyan()
|
||||
);
|
||||
} else {
|
||||
message = format!(
|
||||
"{message} `{}` (v{}) which depends on",
|
||||
step.name.cyan(),
|
||||
step.version.cyan()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
message = format!(
|
||||
"{message} `{}` (v{}) depends on",
|
||||
step.name.cyan(),
|
||||
step.version.cyan()
|
||||
);
|
||||
}
|
||||
range = Some(SentinelRange::from(&step.range).strip());
|
||||
}
|
||||
if let Some(range) = range.filter(|range| *range != Ranges::empty() && *range != Ranges::full())
|
||||
{
|
||||
message = format!("{message} `{}{}`", name.cyan(), range.cyan());
|
||||
} else {
|
||||
message = format!("{message} `{}`", name.cyan());
|
||||
}
|
||||
message
|
||||
}
|
||||
|
||||
/// Render a remote source distribution build failure with a help message.
|
||||
pub(crate) fn download_and_build(sdist: Box<SourceDist>, chain: &DerivationChain, cause: Error) {
|
||||
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||
|
@ -183,16 +234,7 @@ pub(crate) fn download_and_build(sdist: Box<SourceDist>, chain: &DerivationChain
|
|||
if chain.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut message = format!("`{}` was included because", sdist.name().cyan());
|
||||
for (i, step) in chain.iter().enumerate() {
|
||||
if i == 0 {
|
||||
message = format!("{message} `{}` depends on", step.cyan());
|
||||
} else {
|
||||
message = format!("{message} `{}` which depends on", step.cyan());
|
||||
}
|
||||
}
|
||||
message = format!("{message} `{}`", sdist.name().cyan());
|
||||
Some(message)
|
||||
Some(format_chain(sdist.name(), sdist.version(), chain))
|
||||
}
|
||||
}),
|
||||
sdist,
|
||||
|
@ -202,12 +244,12 @@ pub(crate) fn download_and_build(sdist: Box<SourceDist>, chain: &DerivationChain
|
|||
}
|
||||
|
||||
/// Render a remote binary distribution download failure with a help message.
|
||||
pub(crate) fn download(sdist: Box<BuiltDist>, chain: &DerivationChain, cause: Error) {
|
||||
pub(crate) fn download(wheel: Box<BuiltDist>, chain: &DerivationChain, cause: Error) {
|
||||
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||
#[error("Failed to download `{sdist}`")]
|
||||
#[error("Failed to download `{wheel}`")]
|
||||
#[diagnostic()]
|
||||
struct Diagnostic {
|
||||
sdist: Box<BuiltDist>,
|
||||
wheel: Box<BuiltDist>,
|
||||
#[source]
|
||||
cause: Error,
|
||||
#[help]
|
||||
|
@ -216,11 +258,11 @@ pub(crate) fn download(sdist: Box<BuiltDist>, chain: &DerivationChain, cause: Er
|
|||
|
||||
let report = miette::Report::new(Diagnostic {
|
||||
help: SUGGESTIONS
|
||||
.get(sdist.name())
|
||||
.get(wheel.name())
|
||||
.map(|suggestion| {
|
||||
format!(
|
||||
"`{}` is often confused for `{}` Did you mean to install `{}` instead?",
|
||||
sdist.name().cyan(),
|
||||
wheel.name().cyan(),
|
||||
suggestion.cyan(),
|
||||
suggestion.cyan(),
|
||||
)
|
||||
|
@ -229,19 +271,10 @@ pub(crate) fn download(sdist: Box<BuiltDist>, chain: &DerivationChain, cause: Er
|
|||
if chain.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut message = format!("`{}` was included because", sdist.name().cyan());
|
||||
for (i, step) in chain.iter().enumerate() {
|
||||
if i == 0 {
|
||||
message = format!("{message} `{}` depends on", step.cyan());
|
||||
} else {
|
||||
message = format!("{message} `{}` which depends on", step.cyan());
|
||||
}
|
||||
}
|
||||
message = format!("{message} `{}`", sdist.name().cyan());
|
||||
Some(message)
|
||||
Some(format_chain(wheel.name(), Some(wheel.version()), chain))
|
||||
}
|
||||
}),
|
||||
sdist,
|
||||
wheel,
|
||||
cause,
|
||||
});
|
||||
anstream::eprint!("{report:?}");
|
||||
|
@ -275,16 +308,7 @@ pub(crate) fn build(sdist: Box<SourceDist>, chain: &DerivationChain, cause: Erro
|
|||
if chain.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut message = format!("`{}` was included because", sdist.name().cyan());
|
||||
for (i, step) in chain.iter().enumerate() {
|
||||
if i == 0 {
|
||||
message = format!("{message} `{}` depends on", step.cyan());
|
||||
} else {
|
||||
message = format!("{message} `{}` which depends on", step.cyan());
|
||||
}
|
||||
}
|
||||
message = format!("{message} `{}`", sdist.name().cyan());
|
||||
Some(message)
|
||||
Some(format_chain(sdist.name(), sdist.version(), chain))
|
||||
}
|
||||
}),
|
||||
sdist,
|
||||
|
|
|
@ -5574,7 +5574,7 @@ fn fail_to_add_revert_project() -> Result<()> {
|
|||
File "<string>", line 1, in <module>
|
||||
ZeroDivisionError: division by zero
|
||||
|
||||
help: `child` was included because `parent==0.1.0` depends on `child`
|
||||
help: `child` was included because `parent` (v0.1.0) depends on `child`
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
@ -5683,7 +5683,7 @@ fn fail_to_edit_revert_project() -> Result<()> {
|
|||
File "<string>", line 1, in <module>
|
||||
ZeroDivisionError: division by zero
|
||||
|
||||
help: `child` was included because `parent==0.1.0` depends on `child`
|
||||
help: `child` was included because `parent` (v0.1.0) depends on `child`
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
|
|
@ -7357,7 +7357,7 @@ fn lock_invalid_hash() -> Result<()> {
|
|||
|
||||
Computed:
|
||||
sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||||
help: `idna` was included because `project==0.1.0` depends on `anyio==3.7.0` which depends on `idna`
|
||||
help: `idna` (v3.6) was included because `project` (v0.1.0) depends on `anyio` (v3.7.0) which depends on `idna`
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
@ -8196,7 +8196,7 @@ fn lock_redact_https() -> Result<()> {
|
|||
× Failed to download `iniconfig==2.0.0`
|
||||
├─▶ Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
|
||||
╰─▶ HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
|
||||
help: `iniconfig` was included because `foo==0.1.0` depends on `iniconfig`
|
||||
help: `iniconfig` (v2.0.0) was included because `foo` (v0.1.0) depends on `iniconfig`
|
||||
"###);
|
||||
|
||||
// Installing from the lockfile should fail without an index.
|
||||
|
@ -8209,7 +8209,7 @@ fn lock_redact_https() -> Result<()> {
|
|||
× Failed to download `iniconfig==2.0.0`
|
||||
├─▶ Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
|
||||
╰─▶ HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
|
||||
help: `iniconfig` was included because `foo==0.1.0` depends on `iniconfig`
|
||||
help: `iniconfig` (v2.0.0) was included because `foo` (v0.1.0) depends on `iniconfig`
|
||||
"###);
|
||||
|
||||
// Installing from the lockfile should succeed when credentials are included on the command-line.
|
||||
|
@ -8249,7 +8249,7 @@ fn lock_redact_https() -> Result<()> {
|
|||
× Failed to download `iniconfig==2.0.0`
|
||||
├─▶ Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
|
||||
╰─▶ HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
|
||||
help: `iniconfig` was included because `foo==0.1.0` depends on `iniconfig`
|
||||
help: `iniconfig` (v2.0.0) was included because `foo` (v0.1.0) depends on `iniconfig`
|
||||
"###);
|
||||
|
||||
// Installing with credentials from with `UV_INDEX_URL` should succeed.
|
||||
|
@ -19832,7 +19832,7 @@ fn lock_dynamic_version() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn lock_derivation_chain() -> Result<()> {
|
||||
fn lock_derivation_chain_prod() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
|
@ -19842,7 +19842,7 @@ fn lock_derivation_chain() -> Result<()> {
|
|||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["wsgiref"]
|
||||
dependencies = ["wsgiref==0.1.2"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
|
@ -19882,7 +19882,7 @@ fn lock_derivation_chain() -> Result<()> {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||||
|
||||
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
|
||||
help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref==0.1.2`
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
@ -19900,7 +19900,7 @@ fn lock_derivation_chain_extra() -> Result<()> {
|
|||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
optional-dependencies = { wsgi = ["wsgiref"] }
|
||||
optional-dependencies = { wsgi = ["wsgiref>=0.1"] }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
|
@ -19940,7 +19940,7 @@ fn lock_derivation_chain_extra() -> Result<()> {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||||
|
||||
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
|
||||
help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref>=0.1`
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
@ -20000,7 +20000,7 @@ fn lock_derivation_chain_group() -> Result<()> {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||||
|
||||
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
|
||||
help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref`
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
@ -20031,7 +20031,7 @@ fn lock_derivation_chain_extended() -> Result<()> {
|
|||
name = "child"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["wsgiref"]
|
||||
dependencies = ["wsgiref>=0.1, <0.2"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
|
@ -20071,7 +20071,7 @@ fn lock_derivation_chain_extended() -> Result<()> {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||||
|
||||
help: `wsgiref` was included because `project==0.1.0` depends on `child==0.1.0` which depends on `wsgiref`
|
||||
help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `child` (v0.1.0) which depends on `wsgiref>=0.1, <0.2`
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -13292,7 +13292,7 @@ fn compile_derivation_chain() -> Result<()> {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||||
|
||||
help: `wsgiref` was included because `child==0.1.0` depends on `wsgiref`
|
||||
help: `wsgiref` (v0.1.2) was included because `child` (v0.1.0) depends on `wsgiref`
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
|
@ -7372,7 +7372,7 @@ fn resolve_derivation_chain() -> Result<()> {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||||
|
||||
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
|
||||
help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref`
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
|
@ -701,7 +701,7 @@ fn sync_build_isolation_package() -> Result<()> {
|
|||
File "<string>", line 8, in <module>
|
||||
ModuleNotFoundError: No module named 'hatchling'
|
||||
|
||||
help: `source-distribution` was included because `project==0.1.0` depends on `source-distribution`
|
||||
help: `source-distribution` was included because `project` (v0.1.0) depends on `source-distribution`
|
||||
"###);
|
||||
|
||||
// Install `hatchling` for `source-distribution`.
|
||||
|
@ -792,7 +792,7 @@ fn sync_build_isolation_extra() -> Result<()> {
|
|||
File "<string>", line 8, in <module>
|
||||
ModuleNotFoundError: No module named 'hatchling'
|
||||
|
||||
help: `source-distribution` was included because `project==0.1.0` depends on `source-distribution`
|
||||
help: `source-distribution` was included because `project` (v0.1.0) depends on `source-distribution`
|
||||
"###);
|
||||
|
||||
// Running `uv sync` with `--all-extras` should also fail.
|
||||
|
@ -811,7 +811,7 @@ fn sync_build_isolation_extra() -> Result<()> {
|
|||
File "<string>", line 8, in <module>
|
||||
ModuleNotFoundError: No module named 'hatchling'
|
||||
|
||||
help: `source-distribution` was included because `project==0.1.0` depends on `source-distribution`
|
||||
help: `source-distribution` was included because `project` (v0.1.0) depends on `source-distribution`
|
||||
"###);
|
||||
|
||||
// Install the build dependencies.
|
||||
|
@ -4334,7 +4334,7 @@ fn sync_derivation_chain() -> Result<()> {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||||
|
||||
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
|
||||
help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref`
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
@ -4398,7 +4398,7 @@ fn sync_derivation_chain_extra() -> Result<()> {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||||
|
||||
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
|
||||
help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref`
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
@ -4464,7 +4464,7 @@ fn sync_derivation_chain_group() -> Result<()> {
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
|
||||
|
||||
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
|
||||
help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref`
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue