//! Given a set of requirements, find a set of compatible packages. use std::fmt::{Display, Formatter}; use std::sync::Arc; use anyhow::Result; use dashmap::{DashMap, DashSet}; use futures::{FutureExt, StreamExt}; 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; use tracing::{debug, info_span, instrument, trace, warn, Instrument}; use url::Url; use crate::candidate_selector::{CandidateDist, CandidateSelector}; use crate::error::ResolveError; use crate::manifest::Manifest; use crate::overrides::Overrides; use crate::pins::FilePins; use crate::pubgrub::{ PubGrubDependencies, PubGrubDistribution, PubGrubPackage, PubGrubPriorities, PubGrubPython, PubGrubSpecifier, }; use crate::python_requirement::PythonRequirement; use crate::resolution::ResolutionGraph; use crate::resolver::allowed_urls::AllowedUrls; pub use crate::resolver::index::InMemoryIndex; use crate::resolver::provider::DefaultResolverProvider; pub use crate::resolver::provider::ResolverProvider; pub(crate) use crate::resolver::provider::VersionsResponse; use crate::resolver::reporter::Facade; pub use crate::resolver::reporter::{BuildId, Reporter}; use crate::yanks::AllowedYanks; use crate::{DependencyMode, Options}; use distribution_filename::WheelFilename; use distribution_types::{ BuiltDist, Dist, DistributionMetadata, IncompatibleWheel, LocalEditable, 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}; use uv_client::{FlatIndex, RegistryClient}; use uv_distribution::DistributionDatabase; use uv_interpreter::Interpreter; use uv_normalize::PackageName; use uv_traits::BuildContext; mod allowed_urls; mod index; mod provider; mod reporter; /// The package version is unavailable and cannot be used /// Unlike [`PackageUnavailable`] this applies to a single version of the package #[derive(Debug, Clone)] pub(crate) enum UnavailableVersion { /// Version is incompatible due to the `Requires-Python` version specifiers for that package. RequiresPython(VersionSpecifiers), /// Version is incompatible because it is yanked Yanked(Yanked), /// Version is incompatible because it has no usable distributions NoDistributions(Option), } /// The package is unavailable and cannot be used #[derive(Debug, Clone)] pub(crate) enum UnavailablePackage { /// Index lookups were disabled (i.e., `--no-index`) and the package was not found in a flat index (i.e. from `--find-links`) NoIndex, /// Network requests were disabled (i.e., `--offline`), and the package was not found in the cache. Offline, /// The package was not found in the registry NotFound, } enum ResolverVersion { /// A usable version Available(Version), /// A version that is not usable for some reaosn Unavailable(Version, UnavailableVersion), } pub struct Resolver<'a, Provider: ResolverProvider> { project: Option, requirements: Vec, constraints: Vec, overrides: Overrides, allowed_yanks: AllowedYanks, allowed_urls: AllowedUrls, dependency_mode: DependencyMode, markers: &'a MarkerEnvironment, python_requirement: PythonRequirement, selector: CandidateSelector, index: &'a InMemoryIndex, /// Incompatibilities for packages that are entirely unavailable unavailable_packages: DashMap, /// The set of all registry-based packages visited during resolution. visited: DashSet, editables: FxHashMap, reporter: Option>, provider: Provider, } impl<'a, Context: BuildContext + Send + Sync> Resolver<'a, DefaultResolverProvider<'a, Context>> { /// Initialize a new resolver using the default backend doing real requests. /// /// Reads the flat index entries. #[allow(clippy::too_many_arguments)] pub fn new( manifest: Manifest, options: Options, markers: &'a MarkerEnvironment, interpreter: &'a Interpreter, tags: &'a Tags, client: &'a RegistryClient, flat_index: &'a FlatIndex, index: &'a InMemoryIndex, build_context: &'a Context, ) -> Self { let provider = DefaultResolverProvider::new( client, DistributionDatabase::new(build_context.cache(), tags, client, build_context), flat_index, tags, PythonRequirement::new(interpreter, markers), options.exclude_newer, build_context.no_binary(), ); Self::new_custom_io( manifest, options, markers, PythonRequirement::new(interpreter, markers), index, provider, ) } } impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> { /// Initialize a new resolver using a user provided backend. pub fn new_custom_io( manifest: Manifest, options: Options, markers: &'a MarkerEnvironment, python_requirement: PythonRequirement, index: &'a InMemoryIndex, provider: Provider, ) -> Self { 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() .map(|(editable, _)| editable.raw()), ) .collect(); // Determine the allowed yanked package versions let allowed_yanks = manifest .requirements .iter() .chain(manifest.constraints.iter()) .collect(); Self { index, unavailable_packages: DashMap::default(), visited: DashSet::default(), selector, allowed_urls, allowed_yanks, dependency_mode: options.dependency_mode, project: manifest.project, requirements: manifest.requirements, constraints: manifest.constraints, overrides: Overrides::from_requirements(manifest.overrides), markers, python_requirement, editables, reporter: None, provider, } } /// Set the [`Reporter`] to use for this installer. #[must_use] pub fn with_reporter(self, reporter: impl Reporter + 'static) -> Self { let reporter = Arc::new(reporter); Self { reporter: Some(reporter.clone()), provider: self.provider.with_reporter(Facade { reporter }), ..self } } /// Resolve a set of requirements into a set of pinned versions. pub async fn resolve(self) -> Result { // A channel to fetch package metadata (e.g., given `flask`, fetch all versions) and version // metadata (e.g., given `flask==1.0.0`, fetch the metadata for that version). // Channel size is set to the same size as the task buffer for simplicity. let (request_sink, request_stream) = tokio::sync::mpsc::channel(50); // Run the fetcher. let requests_fut = self.fetch(request_stream).fuse(); // Run the solver. let resolve_fut = self.solve(&request_sink).fuse(); let resolution = select! { result = requests_fut => { result?; return Err(ResolveError::ChannelClosed); } resolution = resolve_fut => { resolution.map_err(|err| { // Add version information to improve unsat error messages. if let ResolveError::NoSolution(err) = err { ResolveError::NoSolution( err .with_available_versions(&self.python_requirement, &self.visited, &self.index.packages) .with_selector(self.selector.clone()) .with_python_requirement(&self.python_requirement) .with_index_locations(self.provider.index_locations()) .with_unavailable_packages(&self.unavailable_packages) ) } else { err } })? } }; self.on_complete(); Ok(resolution) } /// Run the `PubGrub` solver. #[instrument(skip_all)] async fn solve( &self, request_sink: &tokio::sync::mpsc::Sender, ) -> Result { let root = PubGrubPackage::Root(self.project.clone()); // Keep track of the packages for which we've requested metadata. let mut pins = FilePins::default(); let mut priorities = PubGrubPriorities::default(); // Start the solve. let mut state = State::init(root.clone(), MIN_VERSION.clone()); let mut added_dependencies: FxHashMap> = FxHashMap::default(); let mut next = root; debug!( "Solving with target Python version {}", self.python_requirement.target() ); loop { // Run unit propagation. state.unit_propagation(next)?; // Pre-visit all candidate packages, to allow metadata to be fetched in parallel. Self::pre_visit(state.partial_solution.prioritized_packages(), request_sink).await?; // Choose a package version. let Some(highest_priority_pkg) = state .partial_solution .pick_highest_priority_pkg(|package, _range| { priorities.get(package).unwrap_or_default() }) else { let selection = state.partial_solution.extract_solution(); return ResolutionGraph::from_state( &selection, &pins, &self.index.packages, &self.index.distributions, &self.index.redirects, &state, self.editables.clone(), ); }; next = highest_priority_pkg; let term_intersection = state .partial_solution .term_intersection_for_package(&next) .ok_or_else(|| { PubGrubError::Failure("a package was chosen but we don't have a term.".into()) })?; let decision = self .choose_version( &next, term_intersection.unwrap_positive(), &mut pins, request_sink, ) .await?; // Pick the next compatible version. let version = match decision { None => { debug!("No compatible version found for: {next}"); let term_intersection = state .partial_solution .term_intersection_for_package(&next) .expect("a package was chosen but we don't have a term."); let reason = { if let PubGrubPackage::Package(ref package_name, _, _) = next { // Check if the decision was due to the package being unavailable self.unavailable_packages .get(package_name) .map(|entry| match *entry { UnavailablePackage::NoIndex => { "was not found in the provided package locations" } UnavailablePackage::Offline => "was not found in the cache", UnavailablePackage::NotFound => { "was not found in the package registry" } }) } else { None } }; let inc = Incompatibility::no_versions( next.clone(), term_intersection.clone(), reason.map(ToString::to_string), ); state.add_incompatibility(inc); continue; } Some(version) => version, }; let version = match version { ResolverVersion::Available(version) => version, ResolverVersion::Unavailable(version, unavailable) => { let reason = match unavailable { UnavailableVersion::RequiresPython(requires_python) => { // Incompatible requires-python versions are special in that we track // them as incompatible dependencies instead of marking the package version // as unavailable directly let python_version = requires_python .iter() .map(PubGrubSpecifier::try_from) .fold_ok(Range::full(), |range, specifier| { range.intersection(&specifier.into()) })?; let package = &next; for kind in [PubGrubPython::Installed, PubGrubPython::Target] { state.add_incompatibility(Incompatibility::from_dependency( package.clone(), Range::singleton(version.clone()), (&PubGrubPackage::Python(kind), &python_version), )); } state.partial_solution.add_decision(next.clone(), version); continue; } UnavailableVersion::Yanked(yanked) => match yanked { Yanked::Bool(_) => "it was yanked".to_string(), Yanked::Reason(reason) => format!( "it was yanked (reason: {})", reason.trim().trim_end_matches('.') ), }, UnavailableVersion::NoDistributions(best_incompatible) => { if let Some(best_incompatible) = best_incompatible { match best_incompatible { IncompatibleWheel::NoBinary => "no source distribution is available and using wheels is disabled".to_string(), IncompatibleWheel::RequiresPython => "no wheels are available that meet your required Python version".to_string(), IncompatibleWheel::Tag(tag) => { match tag { IncompatibleTag::Invalid => "no wheels are available with valid tags".to_string(), IncompatibleTag::Python => "no wheels are available with a matching Python implementation".to_string(), IncompatibleTag::Abi => "no wheels are available with a matching Python ABI".to_string(), IncompatibleTag::Platform => "no wheels are available with a matching platform".to_string(), } } } } else { // TODO(zanieb): It's unclear why we would encounter this case still "no wheels are available for your system".to_string() } } }; state.add_incompatibility(Incompatibility::unavailable( next.clone(), version.clone(), reason, )); continue; } }; self.on_progress(&next, &version); if added_dependencies .entry(next.clone()) .or_default() .insert(version.clone()) { // Retrieve that package dependencies. let package = &next; let dependencies = match self .get_dependencies(package, &version, &mut priorities, request_sink) .await? { Dependencies::Unavailable(reason) => { let message = { if matches!(package, PubGrubPackage::Root(_)) { // Including front-matter for the root package is redundant reason.clone() } else { format!("its dependencies are unusable because {reason}") } }; state.add_incompatibility(Incompatibility::unavailable( package.clone(), version.clone(), message, )); continue; } Dependencies::Available(constraints) if constraints.contains_key(package) => { return Err(PubGrubError::SelfDependency { package: package.clone(), version: version.clone(), } .into()); } Dependencies::Available(constraints) => constraints, }; // Add that package and version if the dependencies are not problematic. let dep_incompats = state.add_incompatibility_from_dependencies( package.clone(), version.clone(), &dependencies, ); state.partial_solution.add_version( package.clone(), version, dep_incompats, &state.incompatibility_store, ); } else { // `dep_incompats` are already in `incompatibilities` so we know there are not satisfied // terms and can add the decision directly. state.partial_solution.add_decision(next.clone(), version); } } } /// Visit a [`PubGrubPackage`] prior to selection. This should be called on a [`PubGrubPackage`] /// before it is selected, to allow metadata to be fetched in parallel. async fn visit_package( &self, package: &PubGrubPackage, priorities: &mut PubGrubPriorities, request_sink: &tokio::sync::mpsc::Sender, ) -> Result<(), ResolveError> { match package { PubGrubPackage::Root(_) => {} PubGrubPackage::Python(_) => {} PubGrubPackage::Package(package_name, _extra, None) => { // Emit a request to fetch the metadata for this package. if self.index.packages.register(package_name.clone()) { priorities.add(package_name.clone()); request_sink .send(Request::Package(package_name.clone())) .await?; } } PubGrubPackage::Package(package_name, _extra, Some(url)) => { // Emit a request to fetch the metadata for this distribution. let dist = Dist::from_url(package_name.clone(), url.clone())?; if self.index.distributions.register(dist.package_id()) { priorities.add(dist.name().clone()); request_sink.send(Request::Dist(dist)).await?; } } } Ok(()) } /// Visit the set of [`PubGrubPackage`] candidates prior to selection. This allows us to fetch /// metadata for all of the packages in parallel. async fn pre_visit<'data>( packages: impl Iterator)>, request_sink: &tokio::sync::mpsc::Sender, ) -> Result<(), ResolveError> { // Iterate over the potential packages, and fetch file metadata for any of them. These // represent our current best guesses for the versions that we _might_ select. for (package, range) in packages { let PubGrubPackage::Package(package_name, _extra, None) = package else { continue; }; request_sink .send(Request::Prefetch(package_name.clone(), range.clone())) .await?; } Ok(()) } /// Given a set of candidate packages, choose the next package (and version) to add to the /// partial solution. /// /// Returns [None] when there are no versions in the given range. #[instrument(skip_all, fields(%package))] async fn choose_version( &self, package: &PubGrubPackage, range: &Range, pins: &mut FilePins, request_sink: &tokio::sync::mpsc::Sender, ) -> Result, ResolveError> { match package { PubGrubPackage::Root(_) => Ok(Some(ResolverVersion::Available(MIN_VERSION.clone()))), PubGrubPackage::Python(PubGrubPython::Installed) => { let version = self.python_requirement.installed(); if range.contains(version) { Ok(Some(ResolverVersion::Available(version.clone()))) } else { Ok(None) } } PubGrubPackage::Python(PubGrubPython::Target) => { let version = self.python_requirement.target(); if range.contains(version) { Ok(Some(ResolverVersion::Available(version.clone()))) } else { Ok(None) } } PubGrubPackage::Package(package_name, extra, Some(url)) => { if let Some(extra) = extra { debug!( "Searching for a compatible version of {package_name}[{extra}] @ {url} ({range})", ); } else { debug!( "Searching for a compatible version of {package_name} @ {url} ({range})" ); } // 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 let Ok(wheel_filename) = WheelFilename::try_from(url.raw()) { // If the URL is that of a wheel, extract the version. let version = wheel_filename.version; if range.contains(&version) { Ok(Some(ResolverVersion::Available(version))) } else { Ok(None) } } else { // Otherwise, assume this is a source distribution. let dist = PubGrubDistribution::from_url(package_name, url); let metadata = self .index .distributions .wait(&dist.package_id()) .await .ok_or(ResolveError::Unregistered)?; let version = &metadata.version; if range.contains(version) { Ok(Some(ResolverVersion::Available(version.clone()))) } else { Ok(None) } } } 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 .packages .wait(package_name) .instrument(info_span!("package_wait", %package_name)) .await .ok_or(ResolveError::Unregistered)?; self.visited.insert(package_name.clone()); let version_map = match *versions_response { VersionsResponse::Found(ref version_map) => version_map, // Short-circuit if we do not find any versions for the package VersionsResponse::NoIndex => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::NoIndex); return Ok(None); } VersionsResponse::Offline => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::Offline); return Ok(None); } VersionsResponse::NotFound => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::NotFound); return Ok(None); } }; if let Some(extra) = extra { debug!( "Searching for a compatible version of {package_name}[{extra}] ({range})", ); } else { debug!("Searching for a compatible version of {package_name} ({range})"); } // Find a version. let Some(candidate) = self.selector.select(package_name, range, version_map) else { // Short circuit: we couldn't find _any_ versions for a package. return Ok(None); }; let dist = match candidate.dist() { CandidateDist::Compatible(dist) => dist, CandidateDist::ExcludeNewer => { // If the version is incomatible because of `exclude_newer`, pretend the versions do not exist return Ok(None); } CandidateDist::Incompatible(incompatibility) => { // If the version is incompatible because no distributions match, exit early. return Ok(Some(ResolverVersion::Unavailable( candidate.version().clone(), UnavailableVersion::NoDistributions(incompatibility.cloned()), ))); } }; // If the version is incompatible because it was yanked, exit early. if dist.yanked().is_yanked() { if self .allowed_yanks .allowed(package_name, candidate.version()) { warn!("Allowing yanked version: {}", candidate.package_id()); } else { return Ok(Some(ResolverVersion::Unavailable( candidate.version().clone(), UnavailableVersion::Yanked(dist.yanked().clone()), ))); } } // If the version is incompatible because of its Python requirement if let Some(requires_python) = self.python_requirement.validate_dist(dist) { return Ok(Some(ResolverVersion::Unavailable( candidate.version().clone(), UnavailableVersion::RequiresPython(requires_python.clone()), ))); } if let Some(extra) = extra { debug!( "Selecting: {}[{}]=={} ({})", candidate.name(), extra, candidate.version(), dist.for_resolution() .dist .filename() .unwrap_or("unknown filename") ); } else { debug!( "Selecting: {}=={} ({})", candidate.name(), candidate.version(), dist.for_resolution() .dist .filename() .unwrap_or("unknown filename") ); } // We want to return a package pinned to a specific version; but we _also_ want to // store the exact file that we selected to satisfy that version. pins.insert(&candidate, dist); let version = candidate.version().clone(); // Emit a request to fetch the metadata for this version. if self.index.distributions.register(candidate.package_id()) { let dist = dist.for_resolution().dist.clone(); request_sink.send(Request::Dist(dist)).await?; } Ok(Some(ResolverVersion::Available(version))) } } } /// Given a candidate package and version, return its dependencies. #[instrument(skip_all, fields(%package, %version))] async fn get_dependencies( &self, package: &PubGrubPackage, version: &Version, priorities: &mut PubGrubPriorities, request_sink: &tokio::sync::mpsc::Sender, ) -> Result { match package { PubGrubPackage::Root(_) => { // Add the root requirements. let constraints = PubGrubDependencies::from_requirements( &self.requirements, &self.constraints, &self.overrides, None, None, self.markers, ); if let Err( err @ (ResolveError::ConflictingVersions(..) | ResolveError::ConflictingUrls(..)), ) = constraints { return Ok(Dependencies::Unavailable(uncapitalize(err.to_string()))); } let mut constraints = constraints?; for (package, version) in constraints.iter() { debug!("Adding direct dependency: {package}{version}"); // Emit a request to fetch the metadata for this package. self.visit_package(package, priorities, request_sink) .await?; } // Add a dependency on each editable. for (editable, metadata) in self.editables.values() { constraints.insert( PubGrubPackage::Package(metadata.name.clone(), None, None), Range::singleton(metadata.version.clone()), ); for extra in &editable.extras { constraints.insert( PubGrubPackage::Package( metadata.name.clone(), Some(extra.clone()), None, ), Range::singleton(metadata.version.clone()), ); } } Ok(Dependencies::Available(constraints.into())) } PubGrubPackage::Python(_) => { Ok(Dependencies::Available(DependencyConstraints::default())) } PubGrubPackage::Package(package_name, extra, url) => { // If we're excluding transitive dependencies, short-circuit. if self.dependency_mode.is_direct() { return Ok(Dependencies::Available(DependencyConstraints::default())); } // Determine if the distribution is editable. if let Some((_local, metadata)) = self.editables.get(package_name) { let mut constraints = PubGrubDependencies::from_requirements( &metadata.requires_dist, &self.constraints, &self.overrides, Some(package_name), extra.as_ref(), self.markers, )?; for (package, version) in constraints.iter() { debug!("Adding transitive dependency: {package}{version}"); // Emit a request to fetch the metadata for this package. self.visit_package(package, priorities, request_sink) .await?; } // 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), Range::singleton(version.clone()), ); } return Ok(Dependencies::Available(constraints.into())); } // Determine the distribution to lookup. let dist = match url { Some(url) => PubGrubDistribution::from_url(package_name, url), None => PubGrubDistribution::from_registry(package_name, version), }; let package_id = dist.package_id(); // If the package does not exist in the registry, we cannot fetch its dependencies if self.unavailable_packages.get(package_name).is_some() { debug_assert!( false, "Dependencies were requested for a package that is not available" ); return Ok(Dependencies::Unavailable( "The package is unavailable".to_string(), )); } // Wait for the metadata to be available. let metadata = self .index .distributions .wait(&package_id) .instrument(info_span!("distributions_wait", %package_id)) .await .ok_or(ResolveError::Unregistered)?; let mut constraints = PubGrubDependencies::from_requirements( &metadata.requires_dist, &self.constraints, &self.overrides, Some(package_name), extra.as_ref(), self.markers, )?; for (package, version) in constraints.iter() { debug!("Adding transitive dependency: {package}{version}"); // Emit a request to fetch the metadata for this package. self.visit_package(package, priorities, request_sink) .await?; } // 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), Range::singleton(version.clone()), ); } Ok(Dependencies::Available(constraints.into())) } } } /// Fetch the metadata for a stream of packages and versions. async fn fetch( &self, request_stream: tokio::sync::mpsc::Receiver, ) -> Result<(), ResolveError> { let mut response_stream = ReceiverStream::new(request_stream) .map(|request| self.process_request(request).boxed()) .buffer_unordered(50); while let Some(response) = response_stream.next().await { match response? { Some(Response::Package(package_name, version_map)) => { trace!("Received package metadata for: {package_name}"); self.index.packages.done(package_name, version_map); } Some(Response::Dist { dist: Dist::Built(dist), metadata, precise: _, }) => { trace!("Received built distribution metadata for: {dist}"); self.index.distributions.done(dist.package_id(), metadata); } Some(Response::Dist { dist: Dist::Source(distribution), metadata, precise, }) => { trace!("Received source distribution metadata for: {distribution}"); self.index .distributions .done(distribution.package_id(), metadata); if let Some(precise) = precise { match distribution { SourceDist::DirectUrl(sdist) => { self.index.redirects.insert(sdist.url.to_url(), precise); } SourceDist::Git(sdist) => { self.index.redirects.insert(sdist.url.to_url(), precise); } SourceDist::Path(sdist) => { self.index.redirects.insert(sdist.url.to_url(), precise); } SourceDist::Registry(_) => {} } } } None => {} } } Ok::<(), ResolveError>(()) } #[instrument(skip_all, fields(%request))] async fn process_request(&self, request: Request) -> Result, ResolveError> { match request { // Fetch package metadata from the registry. Request::Package(package_name) => { let package_versions = self .provider .get_package_versions(&package_name) .boxed() .await .map_err(ResolveError::Client)?; Ok(Some(Response::Package(package_name, package_versions))) } // Fetch distribution metadata from the distribution database. Request::Dist(dist) => { let (metadata, precise) = self .provider .get_or_build_wheel_metadata(&dist) .boxed() .await .map_err(|err| match dist.clone() { Dist::Built(BuiltDist::Path(built_dist)) => { ResolveError::Read(Box::new(built_dist), err) } Dist::Source(SourceDist::Path(source_dist)) => { ResolveError::Build(Box::new(source_dist), err) } Dist::Built(built_dist) => ResolveError::Fetch(Box::new(built_dist), err), Dist::Source(source_dist) => { ResolveError::FetchAndBuild(Box::new(source_dist), err) } })?; Ok(Some(Response::Dist { dist, metadata, precise, })) } // 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 .packages .wait(&package_name) .await .ok_or(ResolveError::Unregistered)?; let version_map = match *versions_response { VersionsResponse::Found(ref version_map) => version_map, // Short-circuit if we did not find any versions for the package VersionsResponse::NoIndex => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::NoIndex); return Ok(None); } VersionsResponse::Offline => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::Offline); return Ok(None); } VersionsResponse::NotFound => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::NotFound); return Ok(None); } }; // Try to find a compatible version. If there aren't any compatible versions, // short-circuit and return `None`. let Some(candidate) = self.selector.select(&package_name, &range, version_map) else { return Ok(None); }; // If there is not a compatible distribution, short-circuit. let Some(dist) = candidate.compatible() else { return Ok(None); }; // If the Python version is incompatible, short-circuit. if self.python_requirement.validate_dist(dist).is_some() { return Ok(None); } // Emit a request to fetch the metadata for this version. if self.index.distributions.register(candidate.package_id()) { let dist = dist.for_resolution().dist.clone(); let (metadata, precise) = self .provider .get_or_build_wheel_metadata(&dist) .boxed() .await .map_err(|err| match dist.clone() { Dist::Built(BuiltDist::Path(built_dist)) => { ResolveError::Read(Box::new(built_dist), err) } Dist::Source(SourceDist::Path(source_dist)) => { ResolveError::Build(Box::new(source_dist), err) } Dist::Built(built_dist) => { ResolveError::Fetch(Box::new(built_dist), err) } Dist::Source(source_dist) => { ResolveError::FetchAndBuild(Box::new(source_dist), err) } })?; Ok(Some(Response::Dist { dist, metadata, precise, })) } else { Ok(None) } } } } fn on_progress(&self, package: &PubGrubPackage, version: &Version) { if let Some(reporter) = self.reporter.as_ref() { match package { PubGrubPackage::Root(_) => {} PubGrubPackage::Python(_) => {} PubGrubPackage::Package(package_name, _extra, Some(url)) => { reporter.on_progress(package_name, VersionOrUrl::Url(url)); } PubGrubPackage::Package(package_name, _extra, None) => { reporter.on_progress(package_name, VersionOrUrl::Version(version)); } } } } fn on_complete(&self) { if let Some(reporter) = self.reporter.as_ref() { reporter.on_complete(); } } } /// Fetch the metadata for an item #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub(crate) enum Request { /// A request to fetch the metadata for a package. Package(PackageName), /// A request to fetch the metadata for a built or source distribution. Dist(Dist), /// A request to pre-fetch the metadata for a package and the best-guess distribution. Prefetch(PackageName, Range), } impl Display for Request { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Request::Package(package_name) => { write!(f, "Versions {package_name}") } Request::Dist(dist) => { write!(f, "Metadata {dist}") } Request::Prefetch(package_name, range) => { write!(f, "Prefetch {package_name} {range}") } } } } #[derive(Debug)] #[allow(clippy::large_enum_variant)] enum Response { /// The returned metadata for a package hosted on a registry. Package(PackageName, VersionsResponse), /// The returned metadata for a distribution. Dist { dist: Dist, metadata: Metadata21, precise: Option, }, } /// An enum used by [`DependencyProvider`] that holds information about package dependencies. /// For each [Package] there is a set of versions allowed as a dependency. #[derive(Clone)] enum Dependencies { /// Package dependencies are not available. Unavailable(String), /// Container for all available package versions. Available(DependencyConstraints>), } fn uncapitalize>(string: T) -> String { let mut chars = string.as_ref().chars(); match chars.next() { None => String::new(), Some(first) => first.to_lowercase().chain(chars).collect(), } }