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:
Charlie Marsh 2024-11-15 15:06:24 -05:00 committed by GitHub
parent a3a543d4f2
commit 8dd095cab8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 235 additions and 198 deletions

2
Cargo.lock generated
View file

@ -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]]

View file

@ -43,3 +43,4 @@ thiserror = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
version-ranges = { workspace = true }

View file

@ -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,
}
}
}

View file

@ -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.

View file

@ -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};

View file

@ -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
};

View file

@ -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 }

View file

@ -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,

View file

@ -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"))?;

View file

@ -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(())

View file

@ -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`
"###
);

View file

@ -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`
"###
);

View file

@ -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(())