diff --git a/Cargo.lock b/Cargo.lock index 040d9598609..9e818a50eb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2457,7 +2457,7 @@ dependencies = [ [[package]] name = "pubgrub" version = "0.2.1" -source = "git+https://github.com/zanieb/pubgrub?rev=9b6d89cb8a0c7902815c8b2ae99106ba322ffb14#9b6d89cb8a0c7902815c8b2ae99106ba322ffb14" +source = "git+https://github.com/zanieb/pubgrub?rev=aab132a3d4d444dd8dd41d8c4e605abd69dacfe1#aab132a3d4d444dd8dd41d8c4e605abd69dacfe1" dependencies = [ "indexmap 2.2.3", "log", diff --git a/Cargo.toml b/Cargo.toml index 34d16170216..008873e387d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ owo-colors = { version = "4.0.0" } petgraph = { version = "0.6.4" } platform-info = { version = "2.0.2" } plist = { version = "1.6.0" } -pubgrub = { git = "https://github.com/zanieb/pubgrub", rev = "9b6d89cb8a0c7902815c8b2ae99106ba322ffb14" } +pubgrub = { git = "https://github.com/zanieb/pubgrub", rev = "aab132a3d4d444dd8dd41d8c4e605abd69dacfe1" } pyo3 = { version = "0.20.2" } pyo3-log = { version = "0.9.0"} pyproject-toml = { version = "0.10.0" } diff --git a/crates/uv-dev/src/resolve_cli.rs b/crates/uv-dev/src/resolve_cli.rs index 449b5bd6c28..de9801f2d5f 100644 --- a/crates/uv-dev/src/resolve_cli.rs +++ b/crates/uv-dev/src/resolve_cli.rs @@ -105,7 +105,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { &flat_index, &index, &build_dispatch, - ); + )?; let resolution_graph = resolver.resolve().await.with_context(|| { format!( "No solution found when resolving: {}", diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 5b8822172a9..320aae87b8b 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -121,7 +121,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.flat_index, self.index, self, - ); + )?; let graph = resolver.resolve().await.with_context(|| { format!( "No solution found when resolving: {}", diff --git a/crates/uv-resolver/src/constraints.rs b/crates/uv-resolver/src/constraints.rs new file mode 100644 index 00000000000..6076f17fbd3 --- /dev/null +++ b/crates/uv-resolver/src/constraints.rs @@ -0,0 +1,30 @@ +use std::hash::BuildHasherDefault; + +use rustc_hash::FxHashMap; + +use pep508_rs::Requirement; +use uv_normalize::PackageName; + +/// A set of constraints for a set of requirements. +#[derive(Debug, Default, Clone)] +pub(crate) struct Constraints(FxHashMap>); + +impl Constraints { + /// Create a new set of constraints from a set of requirements. + pub(crate) fn from_requirements(requirements: Vec) -> Self { + let mut constraints: FxHashMap> = + FxHashMap::with_capacity_and_hasher(requirements.len(), BuildHasherDefault::default()); + for requirement in requirements { + constraints + .entry(requirement.name.clone()) + .or_default() + .push(requirement); + } + Self(constraints) + } + + /// Get the constraints for a package. + pub(crate) fn get(&self, name: &PackageName) -> Option<&Vec> { + self.0.get(name) + } +} diff --git a/crates/uv-resolver/src/editables.rs b/crates/uv-resolver/src/editables.rs new file mode 100644 index 00000000000..b3e162c56d4 --- /dev/null +++ b/crates/uv-resolver/src/editables.rs @@ -0,0 +1,33 @@ +use std::hash::BuildHasherDefault; + +use rustc_hash::FxHashMap; + +use distribution_types::LocalEditable; +use pypi_types::Metadata21; +use uv_normalize::PackageName; + +/// A set of editable packages, indexed by package name. +#[derive(Debug, Default, Clone)] +pub(crate) struct Editables(FxHashMap); + +impl Editables { + /// Create a new set of editables from a set of requirements. + pub(crate) fn from_requirements(requirements: Vec<(LocalEditable, Metadata21)>) -> Self { + let mut editables = + FxHashMap::with_capacity_and_hasher(requirements.len(), BuildHasherDefault::default()); + for (editable_requirement, metadata) in requirements { + editables.insert(metadata.name.clone(), (editable_requirement, metadata)); + } + Self(editables) + } + + /// Get the editable for a package. + pub(crate) fn get(&self, name: &PackageName) -> Option<&(LocalEditable, Metadata21)> { + self.0.get(name) + } + + /// Iterate over all editables. + pub(crate) fn iter(&self) -> impl Iterator { + self.0.values() + } +} diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index c32e247f9ad..9aa1550b3c7 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -7,7 +7,6 @@ use indexmap::IndexMap; use pubgrub::range::Range; use pubgrub::report::{DefaultStringReporter, DerivationTree, Reporter}; use rustc_hash::FxHashMap; -use url::Url; use distribution_types::{BuiltDist, IndexLocations, PathBuiltDist, PathSourceDist, SourceDist}; use once_map::OnceMap; @@ -46,14 +45,20 @@ pub enum ResolveError { #[error("~= operator requires at least two release segments: {0}")] InvalidTildeEquals(pep440_rs::VersionSpecifier), + #[error("Requirements contain conflicting URLs for package `{0}`:\n- {1}\n- {2}")] + ConflictingUrlsDirect(PackageName, String, String), + #[error("There are conflicting URLs for package `{0}`:\n- {1}\n- {2}")] - ConflictingUrls(PackageName, String, String), + ConflictingUrlsTransitive(PackageName, String, String), #[error("There are conflicting versions for `{0}`: {1}")] ConflictingVersions(String, String), #[error("Package `{0}` attempted to resolve via URL: {1}. URL dependencies must be expressed as direct requirements or constraints. Consider adding `{0} @ {1}` to your dependencies or constraints file.")] - DisallowedUrl(PackageName, Url), + DisallowedUrl(PackageName, String), + + #[error("There are conflicting editable requirements for package `{0}`:\n- {1}\n- {2}")] + ConflictingEditables(PackageName, String, String), #[error(transparent)] DistributionType(#[from] distribution_types::Error), diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 97ed74d87c2..b582a423463 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -12,7 +12,9 @@ pub use resolver::{ }; mod candidate_selector; +mod constraints; mod dependency_mode; +mod editables; mod error; mod finder; mod manifest; diff --git a/crates/uv-resolver/src/overrides.rs b/crates/uv-resolver/src/overrides.rs index ca3b1d31a2b..e018d5069cd 100644 --- a/crates/uv-resolver/src/overrides.rs +++ b/crates/uv-resolver/src/overrides.rs @@ -1,6 +1,6 @@ -use itertools::Either; use std::hash::BuildHasherDefault; +use itertools::Either; use rustc_hash::FxHashMap; use pep508_rs::Requirement; diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index ecba3301390..d024d3fcfa8 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -1,32 +1,34 @@ use itertools::Itertools; use pubgrub::range::Range; -use pubgrub::type_aliases::DependencyConstraints; use tracing::warn; +use distribution_types::Verbatim; use pep440_rs::Version; -use pep508_rs::{MarkerEnvironment, Requirement, VerbatimUrl, VersionOrUrl}; +use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; use uv_normalize::{ExtraName, PackageName}; +use crate::constraints::Constraints; use crate::overrides::Overrides; use crate::pubgrub::specifier::PubGrubSpecifier; use crate::pubgrub::PubGrubPackage; +use crate::resolver::Urls; use crate::ResolveError; #[derive(Debug)] -pub struct PubGrubDependencies(DependencyConstraints>); +pub struct PubGrubDependencies(Vec<(PubGrubPackage, Range)>); impl PubGrubDependencies { /// Generate a set of `PubGrub` dependencies from a set of requirements. pub(crate) fn from_requirements( requirements: &[Requirement], - constraints: &[Requirement], + constraints: &Constraints, overrides: &Overrides, source_name: Option<&PackageName>, - source_url: Option<&VerbatimUrl>, source_extra: Option<&ExtraName>, + urls: &Urls, env: &MarkerEnvironment, ) -> Result { - let mut dependencies = DependencyConstraints::>::default(); + let mut dependencies = Vec::default(); // Iterate over all declared requirements. for requirement in overrides.apply(requirements) { @@ -42,100 +44,63 @@ impl PubGrubDependencies { } // Add the package, plus any extra variants. - for result in std::iter::once(to_pubgrub(requirement, None)).chain( + for result in std::iter::once(to_pubgrub(requirement, None, urls)).chain( requirement .extras .clone() .into_iter() - .map(|extra| to_pubgrub(requirement, Some(extra))), + .map(|extra| to_pubgrub(requirement, Some(extra), urls)), ) { let (mut package, version) = result?; // Detect self-dependencies. - if let PubGrubPackage::Package(name, extra, url) = &mut package { + if let PubGrubPackage::Package(name, extra, ..) = &mut package { if source_name.is_some_and(|source_name| source_name == name) { // Allow, e.g., `black` to depend on `black[colorama]`. if source_extra == extra.as_ref() { warn!("{name} has a dependency on itself"); continue; } - // Propagate the source URL. - if source_url.is_some() { - *url = source_url.cloned(); - } } } - if let Some(entry) = dependencies.get_key_value(&package) { - // Merge the versions. - let version = merge_versions(&package, entry.1, &version)?; - - // Merge the package. - if let Some(package) = merge_package(entry.0, &package)? { - dependencies.remove(&package); - dependencies.insert(package, version); - } else { - dependencies.insert(package, version); - } - } else { - dependencies.insert(package.clone(), version.clone()); - } - } - } + dependencies.push((package.clone(), version.clone())); - // If any requirements were further constrained by the user, add those constraints. - for constraint in constraints { - // If a requirement was overridden, skip it. - if overrides.get(&constraint.name).is_some() { - continue; - } - - // If the requirement isn't relevant for the current platform, skip it. - if let Some(extra) = source_extra { - if !constraint.evaluate_markers(env, std::slice::from_ref(extra)) { - continue; - } - } else { - if !constraint.evaluate_markers(env, &[]) { - continue; - } - } - - // Add the package, plus any extra variants. - for result in std::iter::once(to_pubgrub(constraint, None)).chain( - constraint - .extras - .clone() - .into_iter() - .map(|extra| to_pubgrub(constraint, Some(extra))), - ) { - let (mut package, version) = result?; - - // Detect self-dependencies. - if let PubGrubPackage::Package(name, extra, url) = &mut package { - if source_name.is_some_and(|source_name| source_name == name) { - // Allow, e.g., `black` to depend on `black[colorama]`. - if source_extra == extra.as_ref() { - warn!("{name} has a dependency on itself"); + // If the requirement was constrained, add those constraints. + for constraint in constraints.get(&requirement.name).into_iter().flatten() { + // If the requirement isn't relevant for the current platform, skip it. + if let Some(extra) = source_extra { + if !constraint.evaluate_markers(env, std::slice::from_ref(extra)) { continue; } - // Propagate the source URL. - if source_url.is_some() { - *url = source_url.cloned(); + } else { + if !constraint.evaluate_markers(env, &[]) { + continue; } } - } - if let Some(entry) = dependencies.get_key_value(&package) { - // Merge the versions. - let version = merge_versions(&package, entry.1, &version)?; + // Add the package, plus any extra variants. + for result in std::iter::once(to_pubgrub(constraint, None, urls)).chain( + constraint + .extras + .clone() + .into_iter() + .map(|extra| to_pubgrub(constraint, Some(extra), urls)), + ) { + let (mut package, version) = result?; + + // Detect self-dependencies. + if let PubGrubPackage::Package(name, extra, ..) = &mut package { + if source_name.is_some_and(|source_name| source_name == name) { + // Allow, e.g., `black` to depend on `black[colorama]`. + if source_extra == extra.as_ref() { + warn!("{name} has a dependency on itself"); + continue; + } + } + } - // Merge the package. - if let Some(package) = merge_package(entry.0, &package)? { - dependencies.remove(&package); - dependencies.insert(package, version); - } else { - dependencies.insert(package, version); + dependencies.push((package.clone(), version.clone())); } } } @@ -144,23 +109,19 @@ impl PubGrubDependencies { Ok(Self(dependencies)) } - /// Insert a [`PubGrubPackage`] and [`Version`] range into the set of dependencies. - pub(crate) fn insert( - &mut self, - package: PubGrubPackage, - version: Range, - ) -> Option> { - self.0.insert(package, version) + /// Add a [`PubGrubPackage`] and [`PubGrubVersion`] range into the dependencies. + pub(crate) fn push(&mut self, package: PubGrubPackage, version: Range) { + self.0.push((package, version)); } /// Iterate over the dependencies. - pub(crate) fn iter(&self) -> impl Iterator)> { + pub(crate) fn iter(&self) -> impl Iterator)> { self.0.iter() } } /// Convert a [`PubGrubDependencies`] to a [`DependencyConstraints`]. -impl From for DependencyConstraints> { +impl From for Vec<(PubGrubPackage, Range)> { fn from(dependencies: PubGrubDependencies) -> Self { dependencies.0 } @@ -170,18 +131,15 @@ impl From for DependencyConstraints, + urls: &Urls, ) -> Result<(PubGrubPackage, Range), ResolveError> { match requirement.version_or_url.as_ref() { // The requirement has no specifier (e.g., `flask`). None => Ok(( - PubGrubPackage::Package(requirement.name.clone(), extra, None), - Range::full(), - )), - // The requirement has a URL (e.g., `flask @ file:///path/to/flask`). - Some(VersionOrUrl::Url(url)) => Ok(( - PubGrubPackage::Package(requirement.name.clone(), extra, Some(url.clone())), + PubGrubPackage::from_package(requirement.name.clone(), extra, urls), Range::full(), )), + // The requirement has a specifier (e.g., `flask>=1.0`). Some(VersionOrUrl::VersionSpecifier(specifiers)) => { let version = specifiers @@ -191,79 +149,32 @@ fn to_pubgrub( range.intersection(&specifier.into()) })?; Ok(( - PubGrubPackage::Package(requirement.name.clone(), extra, None), + PubGrubPackage::from_package(requirement.name.clone(), extra, urls), version, )) } - } -} - -/// Merge two [`Version`] ranges. -fn merge_versions( - package: &PubGrubPackage, - left: &Range, - right: &Range, -) -> Result, ResolveError> { - let result = left.intersection(right); - if result.is_empty() { - Err(ResolveError::ConflictingVersions( - package.to_string(), - format!("`{package}{left}` does not intersect with `{package}{right}`"), - )) - } else { - Ok(result) - } -} - -/// Merge two [`PubGrubPackage`] instances. -fn merge_package( - left: &PubGrubPackage, - right: &PubGrubPackage, -) -> Result, ResolveError> { - match (left, right) { - // Either package is `root`. - (PubGrubPackage::Root(_), _) | (_, PubGrubPackage::Root(_)) => Ok(None), - - // Either package is the Python installation. - (PubGrubPackage::Python(_), _) | (_, PubGrubPackage::Python(_)) => Ok(None), - - // Left package has a URL. Propagate the URL. - (PubGrubPackage::Package(name, extra, Some(url)), PubGrubPackage::Package(.., None)) => { - Ok(Some(PubGrubPackage::Package( - name.clone(), - extra.clone(), - Some(url.clone()), - ))) - } - - // Right package has a URL. - (PubGrubPackage::Package(.., None), PubGrubPackage::Package(name, extra, Some(url))) => { - Ok(Some(PubGrubPackage::Package( - name.clone(), - extra.clone(), - Some(url.clone()), - ))) - } - // Neither package has a URL. - (PubGrubPackage::Package(_name, _extra, None), PubGrubPackage::Package(.., None)) => { - Ok(None) - } - - // Both packages have a URL. - ( - PubGrubPackage::Package(name, _extra, Some(left)), - PubGrubPackage::Package(.., Some(right)), - ) => { - if cache_key::CanonicalUrl::new(left) == cache_key::CanonicalUrl::new(right) { - Ok(None) - } else { - Err(ResolveError::ConflictingUrls( - name.clone(), - left.to_string(), - right.to_string(), - )) + // The requirement has a URL (e.g., `flask @ file:///path/to/flask`). + Some(VersionOrUrl::Url(url)) => { + let Some(allowed) = urls.get(&requirement.name) else { + return Err(ResolveError::DisallowedUrl( + requirement.name.clone(), + url.verbatim().to_string(), + )); + }; + + if cache_key::CanonicalUrl::new(allowed) != cache_key::CanonicalUrl::new(url) { + return Err(ResolveError::ConflictingUrlsTransitive( + requirement.name.clone(), + allowed.verbatim().to_string(), + url.verbatim().to_string(), + )); } + + Ok(( + PubGrubPackage::Package(requirement.name.clone(), extra, Some(allowed.clone())), + Range::full(), + )) } } } diff --git a/crates/uv-resolver/src/pubgrub/package.rs b/crates/uv-resolver/src/pubgrub/package.rs index 6fb3181adf0..cdec28678c3 100644 --- a/crates/uv-resolver/src/pubgrub/package.rs +++ b/crates/uv-resolver/src/pubgrub/package.rs @@ -3,6 +3,8 @@ use derivative::Derivative; use pep508_rs::VerbatimUrl; use uv_normalize::{ExtraName, PackageName}; +use crate::resolver::Urls; + /// A PubGrub-compatible wrapper around a "Python package", with two notable characteristics: /// /// 1. Includes a [`PubGrubPackage::Root`] variant, to satisfy `PubGrub`'s requirement that a @@ -37,17 +39,7 @@ pub enum PubGrubPackage { /// Additionally, we need to ensure that we disallow multiple versions of the same package, /// even if requested from different URLs. /// - /// Removing the URL from the hash and equality operators is a hack to enable this behavior. - /// When we visit a URL dependency, we include the URL here. But that dependency "looks" - /// equal to the registry version from the solver's perspective, since they hash to the - /// same value. - /// - /// However, this creates the unfortunate requirement that we _must_ visit URL dependencies - /// before their registry variant, so that the URL-based version is used as the "canonical" - /// version. This is because the solver will always prefer the first version it sees, and - /// we need to ensure that the first version is the URL-based version. - /// - /// To enforce this requirement, we in turn require that all possible URL dependencies are + /// To enforce this requirement, we require that all possible URL dependencies are /// defined upfront, as `requirements.txt` or `constraints.txt` or similar. Otherwise, /// solving these graphs becomes far more complicated -- and the "right" behavior isn't /// even clear. For example, imagine that you define a direct dependency on Werkzeug, and @@ -67,13 +59,18 @@ pub enum PubGrubPackage { /// we're going to have a dependency that's provided as a URL, we _need_ to visit the URL /// version before the registry version. So we could just error if we visit a URL variant /// _after_ a registry variant. - #[derivative(PartialEq = "ignore")] - #[derivative(PartialOrd = "ignore")] - #[derivative(Hash = "ignore")] Option, ), } +impl PubGrubPackage { + /// Create a [`PubGrubPackage`] from a package name and optional extra name. + pub(crate) fn from_package(name: PackageName, extra: Option, urls: &Urls) -> Self { + let url = urls.get(&name).cloned(); + Self::Package(name, extra, url) + } +} + #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum PubGrubPython { /// The Python version installed in the current environment. diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index 966b1eac2be..16d3e7753cc 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -19,6 +19,7 @@ use pep508_rs::VerbatimUrl; use pypi_types::{Hashes, Metadata21}; use uv_normalize::{ExtraName, PackageName}; +use crate::editables::Editables; use crate::pins::FilePins; use crate::pubgrub::{PubGrubDistribution, PubGrubPackage, PubGrubPriority}; use crate::resolver::VersionsResponse; @@ -45,7 +46,7 @@ pub struct ResolutionGraph { /// The metadata for every distribution in this resolution. hashes: FxHashMap>, /// The set of editable requirements in this resolution. - editables: FxHashMap, + editables: Editables, /// Any diagnostics that were encountered while building the graph. diagnostics: Vec, } @@ -59,7 +60,7 @@ impl ResolutionGraph { distributions: &OnceMap, redirects: &DashMap, state: &State, PubGrubPriority>, - editables: FxHashMap, + editables: Editables, ) -> Result { // TODO(charlie): petgraph is a really heavy and unnecessary dependency here. We should // write our own graph, given that our requirements are so simple. @@ -100,7 +101,9 @@ impl ResolutionGraph { } PubGrubPackage::Package(package_name, None, Some(url)) => { // Create the distribution. - let pinned_package = { + let pinned_package = if let Some((editable, _)) = editables.get(package_name) { + Dist::from_editable(package_name.clone(), editable.clone())? + } else { let url = redirects.get(url).map_or_else( || url.clone(), |url| VerbatimUrl::unknown(url.value().clone()), @@ -167,24 +170,41 @@ impl ResolutionGraph { PubGrubPackage::Package(package_name, Some(extra), Some(url)) => { // Validate that the `extra` exists. let dist = PubGrubDistribution::from_url(package_name, url); - let metadata = distributions.get(&dist.package_id()).unwrap_or_else(|| { - panic!( - "Every package should have metadata: {:?}", - dist.package_id() - ) - }); - - if !metadata.provides_extras.contains(extra) { - let url = redirects.get(url).map_or_else( - || url.clone(), - |url| VerbatimUrl::unknown(url.value().clone()), - ); - let pinned_package = Dist::from_url(package_name.clone(), url)?; - diagnostics.push(Diagnostic::MissingExtra { - dist: pinned_package, - extra: extra.clone(), + if let Some((_, metadata)) = editables.get(package_name) { + if !metadata.provides_extras.contains(extra) { + let pinned_package = pins + .get(package_name, version) + .unwrap_or_else(|| { + panic!("Every package should be pinned: {package_name:?}") + }) + .clone(); + + diagnostics.push(Diagnostic::MissingExtra { + dist: pinned_package, + extra: extra.clone(), + }); + } + } else { + let metadata = distributions.get(&dist.package_id()).unwrap_or_else(|| { + panic!( + "Every package should have metadata: {:?}", + dist.package_id() + ) }); + + if !metadata.provides_extras.contains(extra) { + let url = redirects.get(url).map_or_else( + || url.clone(), + |url| VerbatimUrl::unknown(url.value().clone()), + ); + let pinned_package = Dist::from_url(package_name.clone(), url)?; + + diagnostics.push(Diagnostic::MissingExtra { + dist: pinned_package, + extra: extra.clone(), + }); + } } } _ => {} diff --git a/crates/uv-resolver/src/resolver/allowed_urls.rs b/crates/uv-resolver/src/resolver/allowed_urls.rs deleted file mode 100644 index 198f3eb414a..00000000000 --- a/crates/uv-resolver/src/resolver/allowed_urls.rs +++ /dev/null @@ -1,17 +0,0 @@ -use rustc_hash::FxHashSet; -use url::Url; - -#[derive(Debug, Default)] -pub(crate) struct AllowedUrls(FxHashSet); - -impl AllowedUrls { - pub(crate) fn contains(&self, url: &Url) -> bool { - self.0.contains(&cache_key::CanonicalUrl::new(url)) - } -} - -impl<'a> FromIterator<&'a Url> for AllowedUrls { - fn from_iter>(iter: T) -> Self { - Self(iter.into_iter().map(cache_key::CanonicalUrl::new).collect()) - } -} diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 2aeb2cf71cb..6c8b69e541d 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -11,7 +11,6 @@ use itertools::Itertools; use pubgrub::error::PubGrubError; use pubgrub::range::Range; use pubgrub::solver::{Incompatibility, State}; -use pubgrub::type_aliases::DependencyConstraints; use rustc_hash::{FxHashMap, FxHashSet}; use tokio::select; use tokio_stream::wrappers::ReceiverStream; @@ -20,13 +19,14 @@ use url::Url; use distribution_filename::WheelFilename; use distribution_types::{ - BuiltDist, Dist, DistributionMetadata, IncompatibleWheel, LocalEditable, Name, RemoteSource, - SourceDist, VersionOrUrl, + BuiltDist, Dist, DistributionMetadata, IncompatibleWheel, Name, RemoteSource, SourceDist, + VersionOrUrl, }; use pep440_rs::{Version, VersionSpecifiers, MIN_VERSION}; use pep508_rs::{MarkerEnvironment, Requirement}; use platform_tags::{IncompatibleTag, Tags}; use pypi_types::{Metadata21, Yanked}; +pub(crate) use urls::Urls; use uv_client::{FlatIndex, RegistryClient}; use uv_distribution::DistributionDatabase; use uv_interpreter::Interpreter; @@ -34,6 +34,8 @@ use uv_normalize::PackageName; use uv_traits::BuildContext; use crate::candidate_selector::{CandidateDist, CandidateSelector}; +use crate::constraints::Constraints; +use crate::editables::Editables; use crate::error::ResolveError; use crate::manifest::Manifest; use crate::overrides::Overrides; @@ -44,7 +46,6 @@ use crate::pubgrub::{ }; use crate::python_requirement::PythonRequirement; use crate::resolution::ResolutionGraph; -use crate::resolver::allowed_urls::AllowedUrls; pub use crate::resolver::index::InMemoryIndex; pub use crate::resolver::provider::DefaultResolverProvider; pub use crate::resolver::provider::ResolverProvider; @@ -54,10 +55,10 @@ pub use crate::resolver::reporter::{BuildId, Reporter}; use crate::yanks::AllowedYanks; use crate::{DependencyMode, Options}; -mod allowed_urls; mod index; mod provider; mod reporter; +mod urls; /// The package version is unavailable and cannot be used /// Unlike [`PackageUnavailable`] this applies to a single version of the package @@ -85,17 +86,18 @@ pub(crate) enum UnavailablePackage { enum ResolverVersion { /// A usable version Available(Version), - /// A version that is not usable for some reaosn + /// A version that is not usable for some reason Unavailable(Version, UnavailableVersion), } pub struct Resolver<'a, Provider: ResolverProvider> { project: Option, requirements: Vec, - constraints: Vec, + constraints: Constraints, overrides: Overrides, + editables: Editables, allowed_yanks: AllowedYanks, - allowed_urls: AllowedUrls, + urls: Urls, dependency_mode: DependencyMode, markers: &'a MarkerEnvironment, python_requirement: PythonRequirement, @@ -105,7 +107,6 @@ pub struct Resolver<'a, Provider: ResolverProvider> { unavailable_packages: DashMap, /// The set of all registry-based packages visited during resolution. visited: DashSet, - editables: FxHashMap, reporter: Option>, provider: Provider, } @@ -125,7 +126,7 @@ impl<'a, Context: BuildContext + Send + Sync> Resolver<'a, DefaultResolverProvid flat_index: &'a FlatIndex, index: &'a InMemoryIndex, build_context: &'a Context, - ) -> Self { + ) -> Result { let provider = DefaultResolverProvider::new( client, DistributionDatabase::new(build_context.cache(), tags, client, build_context), @@ -155,44 +156,9 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { python_requirement: PythonRequirement, index: &'a InMemoryIndex, provider: Provider, - ) -> Self { + ) -> Result { let selector = CandidateSelector::for_resolution(&manifest, options); - // Determine all the editable requirements. - let mut editables = FxHashMap::default(); - for (editable_requirement, metadata) in &manifest.editables { - editables.insert( - metadata.name.clone(), - (editable_requirement.clone(), metadata.clone()), - ); - } - - // Determine the list of allowed URLs. - let allowed_urls: AllowedUrls = manifest - .requirements - .iter() - .chain(manifest.constraints.iter()) - .chain(manifest.overrides.iter()) - .filter_map(|req| { - if let Some(pep508_rs::VersionOrUrl::Url(url)) = &req.version_or_url { - Some(url.raw()) - } else { - None - } - }) - .chain(manifest.editables.iter().flat_map(|(editable, metadata)| { - std::iter::once(editable.raw()).chain(metadata.requires_dist.iter().filter_map( - |req| { - if let Some(pep508_rs::VersionOrUrl::Url(url)) = &req.version_or_url { - Some(url.raw()) - } else { - None - } - }, - )) - })) - .collect(); - // Determine the allowed yanked package versions let allowed_yanks = manifest .requirements @@ -200,24 +166,24 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { .chain(manifest.constraints.iter()) .collect(); - Self { + Ok(Self { index, unavailable_packages: DashMap::default(), visited: DashSet::default(), selector, - allowed_urls, allowed_yanks, dependency_mode: options.dependency_mode, + urls: Urls::from_manifest(&manifest, markers)?, project: manifest.project, requirements: manifest.requirements, - constraints: manifest.constraints, + constraints: Constraints::from_requirements(manifest.constraints), overrides: Overrides::from_requirements(manifest.overrides), + editables: Editables::from_requirements(manifest.editables), markers, python_requirement, - editables, reporter: None, provider, - } + }) } /// Set the [`Reporter`] to use for this installer. @@ -399,7 +365,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { state.add_incompatibility(Incompatibility::from_dependency( package.clone(), Range::singleton(version.clone()), - (&PubGrubPackage::Python(kind), &python_version), + (PubGrubPackage::Python(kind), python_version.clone()), )); } state.partial_solution.add_decision(next.clone(), version); @@ -470,7 +436,11 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { )); continue; } - Dependencies::Available(constraints) if constraints.contains_key(package) => { + Dependencies::Available(constraints) + if constraints + .iter() + .any(|(dependency, _)| dependency == package) => + { return Err(PubGrubError::SelfDependency { package: package.clone(), version: version.clone(), @@ -484,7 +454,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { let dep_incompats = state.add_incompatibility_from_dependencies( package.clone(), version.clone(), - &dependencies, + dependencies, ); state.partial_solution.add_version( @@ -596,12 +566,14 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { ); } - // If the URL wasn't declared in the direct dependencies or constraints, reject it. - if !self.allowed_urls.contains(url) { - return Err(ResolveError::DisallowedUrl( - package_name.clone(), - url.to_url(), - )); + // If the dist is an editable, return the version from the editable metadata. + if let Some((_local, metadata)) = self.editables.get(package_name) { + let version = metadata.version.clone(); + return if range.contains(&version) { + Ok(Some(ResolverVersion::Available(version))) + } else { + Ok(None) + }; } if let Ok(wheel_filename) = WheelFilename::try_from(url.raw()) { @@ -631,16 +603,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { } PubGrubPackage::Package(package_name, extra, None) => { - // If the dist is an editable, return the version from the editable metadata. - if let Some((_local, metadata)) = self.editables.get(package_name) { - let version = metadata.version.clone(); - return if range.contains(&version) { - Ok(Some(ResolverVersion::Available(version))) - } else { - Ok(None) - }; - } - // Wait for the metadata to be available. let versions_response = self .index @@ -784,17 +746,16 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { &self.overrides, None, None, - None, + &self.urls, self.markers, ); - if let Err( - err @ (ResolveError::ConflictingVersions(..) - | ResolveError::ConflictingUrls(..)), - ) = constraints - { - return Ok(Dependencies::Unavailable(uncapitalize(err.to_string()))); - } - let mut constraints = constraints?; + + let mut constraints = match constraints { + Ok(constraints) => constraints, + Err(err) => { + return Ok(Dependencies::Unavailable(uncapitalize(err.to_string()))); + } + }; for (package, version) in constraints.iter() { debug!("Adding direct dependency: {package}{version}"); @@ -805,17 +766,17 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { } // Add a dependency on each editable. - for (editable, metadata) in self.editables.values() { - constraints.insert( - PubGrubPackage::Package(metadata.name.clone(), None, None), + for (editable, metadata) in self.editables.iter() { + constraints.push( + PubGrubPackage::from_package(metadata.name.clone(), None, &self.urls), Range::singleton(metadata.version.clone()), ); for extra in &editable.extras { - constraints.insert( - PubGrubPackage::Package( + constraints.push( + PubGrubPackage::from_package( metadata.name.clone(), Some(extra.clone()), - None, + &self.urls, ), Range::singleton(metadata.version.clone()), ); @@ -825,9 +786,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { Ok(Dependencies::Available(constraints.into())) } - PubGrubPackage::Python(_) => { - Ok(Dependencies::Available(DependencyConstraints::default())) - } + PubGrubPackage::Python(_) => Ok(Dependencies::Available(Vec::default())), PubGrubPackage::Package(package_name, extra, url) => { // If we're excluding transitive dependencies, short-circuit. @@ -851,7 +810,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { .ok_or(ResolveError::Unregistered)?; } - return Ok(Dependencies::Available(DependencyConstraints::default())); + return Ok(Dependencies::Available(Vec::default())); } // Determine if the distribution is editable. @@ -861,8 +820,8 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { &self.constraints, &self.overrides, Some(package_name), - url.as_ref(), extra.as_ref(), + &self.urls, self.markers, )?; @@ -876,8 +835,8 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { // If a package has an extra, insert a constraint on the base package. if extra.is_some() { - constraints.insert( - PubGrubPackage::Package(package_name.clone(), None, None), + constraints.push( + PubGrubPackage::Package(package_name.clone(), None, url.clone()), Range::singleton(version.clone()), ); } @@ -917,8 +876,8 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { &self.constraints, &self.overrides, Some(package_name), - url.as_ref(), extra.as_ref(), + &self.urls, self.markers, )?; @@ -932,7 +891,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { // If a package has an extra, insert a constraint on the base package. if extra.is_some() { - constraints.insert( + constraints.push( PubGrubPackage::Package(package_name.clone(), None, url.clone()), Range::singleton(version.clone()), ); @@ -1040,11 +999,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { // Pre-fetch the package and distribution metadata. Request::Prefetch(package_name, range) => { - // Ignore editables. - if self.editables.contains_key(&package_name) { - return Ok(None); - } - // Wait for the package metadata to become available. let versions_response = self .index @@ -1199,7 +1153,7 @@ enum Dependencies { /// Package dependencies are not available. Unavailable(String), /// Container for all available package versions. - Available(DependencyConstraints>), + Available(Vec<(PubGrubPackage, Range)>), } fn uncapitalize>(string: T) -> String { diff --git a/crates/uv-resolver/src/resolver/urls.rs b/crates/uv-resolver/src/resolver/urls.rs new file mode 100644 index 00000000000..208aeb98fa7 --- /dev/null +++ b/crates/uv-resolver/src/resolver/urls.rs @@ -0,0 +1,83 @@ +use rustc_hash::FxHashMap; + +use distribution_types::Verbatim; +use pep508_rs::{MarkerEnvironment, VerbatimUrl}; +use uv_normalize::PackageName; + +use crate::{Manifest, ResolveError}; + +#[derive(Debug, Default)] +pub(crate) struct Urls(FxHashMap); + +impl Urls { + pub(crate) fn from_manifest( + manifest: &Manifest, + markers: &MarkerEnvironment, + ) -> Result { + let mut urls = FxHashMap::default(); + + // Add all direct requirements and constraints. If there are any conflicts, return an error. + for requirement in manifest + .requirements + .iter() + .chain(manifest.constraints.iter()) + { + if !requirement.evaluate_markers(markers, &[]) { + continue; + } + + if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url { + if let Some(previous) = urls.insert(requirement.name.clone(), url.clone()) { + return Err(ResolveError::ConflictingUrlsDirect( + requirement.name.clone(), + previous.verbatim().to_string(), + url.verbatim().to_string(), + )); + } + } + } + + // Add any editable requirements. If there are any conflicts, return an error. + for (editable_requirement, metadata) in &manifest.editables { + if let Some(previous) = + urls.insert(metadata.name.clone(), editable_requirement.url.clone()) + { + return Err(ResolveError::ConflictingUrlsDirect( + metadata.name.clone(), + previous.verbatim().to_string(), + editable_requirement.url.verbatim().to_string(), + )); + } + + for req in &metadata.requires_dist { + if let Some(pep508_rs::VersionOrUrl::Url(url)) = &req.version_or_url { + if let Some(previous) = urls.insert(req.name.clone(), url.clone()) { + return Err(ResolveError::ConflictingUrlsDirect( + req.name.clone(), + previous.verbatim().to_string(), + url.verbatim().to_string(), + )); + } + } + } + } + + // Add any overrides. Conflicts here are fine, as the overrides are meant to be + // authoritative. + for requirement in &manifest.overrides { + if !requirement.evaluate_markers(markers, &[]) { + continue; + } + + if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url { + urls.insert(requirement.name.clone(), url.clone()); + } + } + + Ok(Self(urls)) + } + + pub(crate) fn get(&self, package: &PackageName) -> Option<&VerbatimUrl> { + self.0.get(package) + } +} diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index fea1e694b98..04f4c7019b5 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -137,7 +137,7 @@ async fn resolve( &flat_index, &index, &build_context, - ); + )?; Ok(resolver.resolve().await?) } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index f1732a914da..12f6a2415e7 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -296,7 +296,7 @@ pub(crate) async fn pip_compile( &flat_index, &top_level_index, &build_dispatch, - ) + )? .with_reporter(ResolverReporter::from(printer)); let resolution = match resolver.resolve().await { diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index eca79b2bab7..693abe2b642 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -454,7 +454,7 @@ async fn resolve( flat_index, index, build_dispatch, - ) + )? .with_reporter(ResolverReporter::from(printer)); let resolution = resolver.resolve().await?; diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 3ee3ff66dda..1408c13ce37 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -1272,15 +1272,41 @@ fn conflicting_repeated_url_dependency_version_mismatch() -> Result<()> { uv_snapshot!(context.compile() .arg("requirements.in"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ your requirements cannot be used because there are conflicting URLs for - package `werkzeug`: - - https://files.pythonhosted.org/packages/bd/24/11c3ea5a7e866bf2d97f0501d0b4b1c9bbeade102bb4b588f0d2919a5212/Werkzeug-2.0.1-py3-none-any.whl - - https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl + error: Requirements contain conflicting URLs for package `werkzeug`: + - https://files.pythonhosted.org/packages/bd/24/11c3ea5a7e866bf2d97f0501d0b4b1c9bbeade102bb4b588f0d2919a5212/Werkzeug-2.0.1-py3-none-any.whl + - https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl + "### + ); + + Ok(()) +} + +/// Request Werkzeug via two different URLs at different versions. However, only one of the +/// URLs is compatible with the requested Python version, so there shouldn't be any conflict. +#[test] +fn conflicting_repeated_url_dependency_markers() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str(indoc! {r" + werkzeug @ https://files.pythonhosted.org/packages/bd/24/11c3ea5a7e866bf2d97f0501d0b4b1c9bbeade102bb4b588f0d2919a5212/Werkzeug-2.0.1-py3-none-any.whl ; python_version >= '3.10' + werkzeug @ https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl ; python_version < '3.10' + "})?; + + uv_snapshot!(context.compile() + .arg("requirements.in"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in + werkzeug @ https://files.pythonhosted.org/packages/bd/24/11c3ea5a7e866bf2d97f0501d0b4b1c9bbeade102bb4b588f0d2919a5212/Werkzeug-2.0.1-py3-none-any.whl + + ----- stderr ----- + Resolved 1 package in [TIME] "### ); @@ -1299,15 +1325,13 @@ fn conflicting_repeated_url_dependency_version_match() -> Result<()> { uv_snapshot!(context.compile() .arg("requirements.in"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ your requirements cannot be used because there are conflicting URLs for - package `werkzeug`: - - git+https://github.com/pallets/werkzeug.git@2.0.0 - - https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl + error: Requirements contain conflicting URLs for package `werkzeug`: + - git+https://github.com/pallets/werkzeug.git@2.0.0 + - https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl "### ); @@ -1418,7 +1442,7 @@ fn allowed_transitive_canonical_url_dependency() -> Result<()> { # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in --constraint constraints.txt transitive-url-dependency @ https://github.com/astral-sh/ruff/files/14078476/transitive_url_dependency.zip - werkzeug @ git+https://github.com/pallets/werkzeug@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74 + werkzeug @ git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74 # via transitive-url-dependency ----- stderr ----- @@ -1598,8 +1622,8 @@ dependencies = ["django==5.0b1", "django==5.0a1"] ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ my-project cannot be used because there are conflicting versions for - `django`: `django==5.0b1` does not intersect with `django==5.0a1` + ╰─▶ Because my-project depends on django==5.0b1 and my-project depends on + django==5.0a1, we can conclude that the requirements are unsatisfiable. "### ); @@ -3720,8 +3744,8 @@ fn compile_types_pytz() -> Result<()> { Ok(()) } -/// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning it to -/// a specific URL. +/// Resolve a package from a `requirements.in` file, with a `constraints.txt` pinning that package +/// to a specific URL. #[test] fn compile_constraints_compatible_url() -> Result<()> { let context = TestContext::new("3.12"); @@ -3754,6 +3778,40 @@ fn compile_constraints_compatible_url() -> Result<()> { Ok(()) } +/// Resolve a direct URL package from a `requirements.in` file, with a `constraints.txt` file +/// pinning it to a specific version. +#[test] +fn compile_constraints_compatible_url_version() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("anyio>4")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--constraint") + .arg("constraints.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in --constraint constraints.txt + anyio @ https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl + idna==3.4 + # via anyio + sniffio==1.3.0 + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + Ok(()) +} + /// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning it to /// a specific URL with an incompatible version. #[test] @@ -3910,3 +3968,262 @@ fn no_deps_invalid_extra() -> Result<()> { Ok(()) } + +/// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning one of +/// its transitive dependencies to a specific version. +#[test] +fn compile_constraints_compatible_version() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("virtualenv")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("filelock==3.8.0")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--constraint") + .arg("constraints.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in --constraint constraints.txt + distlib==0.3.7 + # via virtualenv + filelock==3.8.0 + # via virtualenv + platformdirs==3.11.0 + # via virtualenv + virtualenv==20.21.1 + + ----- stderr ----- + Resolved 4 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning one of +/// its direct dependencies to an incompatible version. +#[test] +fn compile_constraints_incompatible_version() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("filelock==1.0.0")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("filelock==3.8.0")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--constraint") + .arg("constraints.txt"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because you require filelock==1.0.0 and you require filelock==3.8.0, we + can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Resolve a package from a `requirements.in` file, with a `constraints.txt` file pinning one of +/// its direct dependencies to an incompatible version. +#[test] +fn conflicting_url_markers() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("filelock==1.0.0")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("filelock==3.8.0")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--constraint") + .arg("constraints.txt"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because you require filelock==1.0.0 and you require filelock==3.8.0, we + can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Override a regular package with an editable. +/// +/// At present, this incorrectly resolves to the regular package. +#[test] +fn editable_override() -> Result<()> { + let context = TestContext::new("3.12"); + + // Add a non-editable requirement. + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("black")?; + + // Add an editable override. + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("-e file://../../scripts/editable-installs/black_editable")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--override") + .arg("overrides.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in --override overrides.txt + black==23.11.0 + click==8.1.7 + # via black + mypy-extensions==1.0.0 + # via black + packaging==23.2 + # via black + pathspec==0.11.2 + # via black + platformdirs==4.0.0 + # via black + + ----- stderr ----- + Resolved 6 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Override an editable with a regular package. +/// +/// At present, this incorrectly resolves to the editable. +#[test] +fn override_editable() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("-e ../../scripts/editable-installs/black_editable")?; + + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("black==23.10.1")?; + + let requirements_path = regex::escape(&requirements_in.normalized_display().to_string()); + let overrides_path = regex::escape(&overrides_txt.normalized_display().to_string()); + let filters: Vec<_> = [ + (requirements_path.as_str(), "requirements.in"), + (overrides_path.as_str(), "overrides.txt"), + ] + .into_iter() + .chain(INSTA_FILTERS.to_vec()) + .collect(); + + uv_snapshot!(filters, Command::new(get_bin()) + .arg("pip") + .arg("compile") + .arg(requirements_in.path()) + .arg("--override") + .arg(overrides_txt.path()) + .arg("--cache-dir") + .arg(context.cache_dir.path()) + .arg("--exclude-newer") + .arg(EXCLUDE_NEWER) + .env("VIRTUAL_ENV", context.venv.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile requirements.in --override overrides.txt --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z + -e ../../scripts/editable-installs/black_editable + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 1 package in [TIME] + "###); + + Ok(()) +} + +/// Resolve a package with both a constraint _and_ an override. The override and the constraint are +/// compatible, but resolve to exactly the same version. +#[test] +fn override_with_compatible_constraint() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("anyio<=3.0.0")?; + + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("anyio>=3.0.0")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--constraint") + .arg("constraints.txt") + .arg("--override") + .arg("overrides.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2023-11-18T12:00:00Z requirements.in --constraint constraints.txt --override overrides.txt + anyio==3.0.0 + idna==3.4 + # via anyio + sniffio==1.3.0 + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Resolve a package with both a constraint _and_ an override. The override and the constraint are +/// incompatible, and so should error. (The correctness of this behavior is subject to debate.) +#[test] +fn override_with_incompatible_constraint() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("anyio<3.0.0")?; + + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("anyio>=3.0.0")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--constraint") + .arg("constraints.txt") + .arg("--override") + .arg("overrides.txt"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because you require anyio>=3.0.0 and you require anyio<3.0.0, we can + conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 98f8d2cb77b..f7edfd6216a 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -361,13 +361,13 @@ fn excluded_only_compatible_version() { albatross<2.0.0 albatross>2.0.0 depends on one of: - bluebird<=1.0.0 - bluebird>=3.0.0 + bluebird==1.0.0 + bluebird==3.0.0 - And because you require bluebird>=2.0.0,<3.0.0 and you require one of: + And because you require one of: albatross<2.0.0 albatross>2.0.0 - we can conclude that the requirements are unsatisfiable. + and you require bluebird>=2.0.0,<3.0.0, we can conclude that the requirements are unsatisfiable. "###); // Only `a==1.2.0` is available since `a==1.0.0` and `a==3.0.0` require @@ -465,12 +465,12 @@ fn dependency_excludes_range_of_compatible_versions() { albatross<2.0.0 albatross>=3.0.0 - And because we know from (1) that albatross<2.0.0 depends on bluebird==1.0.0, we can conclude that albatross!=3.0.0, all versions of crow, bluebird!=1.0.0 are incompatible. + And because we know from (1) that albatross<2.0.0 depends on bluebird==1.0.0, we can conclude that bluebird!=1.0.0, albatross!=3.0.0, all versions of crow are incompatible. And because albatross==3.0.0 depends on bluebird==3.0.0, we can conclude that all versions of crow depend on one of: - bluebird==1.0.0 - bluebird==3.0.0 + bluebird<=1.0.0 + bluebird>=3.0.0 - And because you require crow and you require bluebird>=2.0.0,<3.0.0, we can conclude that the requirements are unsatisfiable. + And because you require bluebird>=2.0.0,<3.0.0 and you require crow, we can conclude that the requirements are unsatisfiable. "###); // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` @@ -567,20 +567,21 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of crow are available: + ╰─▶ Because only the following versions of albatross are available: + albatross==1.0.0 + albatross>=2.0.0,<=3.0.0 + and albatross==1.0.0 depends on bluebird==1.0.0, we can conclude that albatross<2.0.0 depends on bluebird==1.0.0. (1) + + Because only the following versions of crow are available: crow==1.0.0 crow==2.0.0 - and crow==1.0.0 depends on albatross<2.0.0, we can conclude that crow<2.0.0 depends on albatross<2.0.0. (1) - - Because only the following versions of albatross are available: - albatross==1.0.0 - albatross>=2.0.0 - and albatross==1.0.0 depends on bluebird==1.0.0, we can conclude that albatross<2.0.0 depends on bluebird==1.0.0. - And because we know from (1) that crow<2.0.0 depends on albatross<2.0.0, we can conclude that crow<2.0.0 depends on bluebird==1.0.0. - And because crow==2.0.0 depends on albatross>=3.0.0, we can conclude that albatross<3.0.0, all versions of crow, bluebird!=1.0.0 are incompatible. (2) + and crow==1.0.0 depends on albatross<2.0.0, we can conclude that crow<2.0.0 depends on albatross<2.0.0. + And because crow==2.0.0 depends on albatross>=3.0.0, we can conclude that all versions of crow depend on one of: + albatross<2.0.0 + albatross>=3.0.0 - Because only albatross<=3.0.0 is available and albatross==3.0.0 depends on bluebird==3.0.0, we can conclude that albatross>=3.0.0 depends on bluebird==3.0.0. - And because we know from (2) that albatross<3.0.0, all versions of crow, bluebird!=1.0.0 are incompatible, we can conclude that all versions of crow depend on one of: + And because we know from (1) that albatross<2.0.0 depends on bluebird==1.0.0, we can conclude that bluebird!=1.0.0, all versions of crow, albatross!=3.0.0 are incompatible. + And because albatross==3.0.0 depends on bluebird==3.0.0, we can conclude that all versions of crow depend on one of: bluebird<=1.0.0 bluebird>=3.0.0 @@ -859,9 +860,9 @@ fn extra_incompatible_with_extra() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only albatross[extra-b]==1.0.0 is available and albatross[extra-b]==1.0.0 depends on bluebird==1.0.0, we can conclude that all versions of albatross[extra-b] depend on bluebird==1.0.0. - And because albatross[extra-c]==1.0.0 depends on bluebird==2.0.0 and only albatross[extra-c]==1.0.0 is available, we can conclude that all versions of albatross[extra-c] and all versions of albatross[extra-b] are incompatible. - And because you require albatross[extra-c] and you require albatross[extra-b], we can conclude that the requirements are unsatisfiable. + ╰─▶ Because only albatross[extra-c]==1.0.0 is available and albatross[extra-c]==1.0.0 depends on bluebird==2.0.0, we can conclude that all versions of albatross[extra-c] depend on bluebird==2.0.0. + And because albatross[extra-b]==1.0.0 depends on bluebird==1.0.0 and only albatross[extra-b]==1.0.0 is available, we can conclude that all versions of albatross[extra-c] and all versions of albatross[extra-b] are incompatible. + And because you require albatross[extra-b] and you require albatross[extra-c], we can conclude that the requirements are unsatisfiable. "###); // Because both `extra_b` and `extra_c` are requested and they require incompatible @@ -1069,7 +1070,7 @@ fn direct_incompatible_versions() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ your requirements cannot be used because there are conflicting versions for `albatross`: `albatross==1.0.0` does not intersect with `albatross==2.0.0` + ╰─▶ Because you require albatross==1.0.0 and you require albatross==2.0.0, we can conclude that the requirements are unsatisfiable. "###); assert_not_installed(&context.venv, "a_c0e7adfa", &context.temp_dir); @@ -1173,9 +1174,9 @@ fn transitive_incompatible_with_transitive() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only bluebird==1.0.0 is available and bluebird==1.0.0 depends on crow==2.0.0, we can conclude that all versions of bluebird depend on crow==2.0.0. - And because albatross==1.0.0 depends on crow==1.0.0 and only albatross==1.0.0 is available, we can conclude that all versions of bluebird and all versions of albatross are incompatible. - And because you require bluebird and you require albatross, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 depends on crow==1.0.0, we can conclude that all versions of albatross depend on crow==1.0.0. + And because bluebird==1.0.0 depends on crow==2.0.0 and only bluebird==1.0.0 is available, we can conclude that all versions of bluebird and all versions of albatross are incompatible. + And because you require albatross and you require bluebird, we can conclude that the requirements are unsatisfiable. "###); assert_not_installed(&context.venv, "a_ec82e315", &context.temp_dir); @@ -1940,10 +1941,10 @@ fn transitive_prerelease_and_stable_dependency_many_versions() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only bluebird==1.0.0 is available and bluebird==1.0.0 depends on crow, we can conclude that all versions of bluebird depend on crow. - And because only crow<2.0.0b1 is available, we can conclude that all versions of bluebird depend on crow<2.0.0b1. - And because albatross==1.0.0 depends on crow>=2.0.0b1 and only albatross==1.0.0 is available, we can conclude that all versions of bluebird and all versions of albatross are incompatible. - And because you require bluebird and you require albatross, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because only albatross==1.0.0 is available and albatross==1.0.0 depends on crow>=2.0.0b1, we can conclude that all versions of albatross depend on crow>=2.0.0b1. + And because only crow<2.0.0b1 is available, we can conclude that all versions of albatross depend on crow>3.0.0. + And because bluebird==1.0.0 depends on crow and only bluebird==1.0.0 is available, we can conclude that all versions of albatross and all versions of bluebird are incompatible. + And because you require albatross and you require bluebird, we can conclude that the requirements are unsatisfiable. hint: crow was requested with a pre-release marker (e.g., crow>=2.0.0b1), but pre-releases weren't enabled (try: `--prerelease=allow`) "###);