mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
Avoid hard-error for non-existent extras (#627)
## Summary When resolving `transformers[tensorboard]`, the `[tensorboard]` extra doesn't exist. Previously, we returned "unknown" dependencies for this variant, which leads the resolution to try all versions, then fail. This PR instead warns, but returns the base dependencies for the package, which matches `pip`. (Poetry doesn't even warn, it just proceeds as normal.) Arguably, it would be better to return a custom incompatibility here and then propagate... But this PR is better than the status quo, and I don't know if we have support for that behavior yet...? (\cc @zanieb) Closes #386. Closes https://github.com/astral-sh/puffin/issues/423.
This commit is contained in:
parent
5c38825b93
commit
eb1a630db2
11 changed files with 351 additions and 96 deletions
|
@ -178,6 +178,18 @@ pub(crate) async fn pip_compile(
|
|||
.dimmed()
|
||||
)?;
|
||||
|
||||
// Notify the user of any diagnostics.
|
||||
for diagnostic in resolution.diagnostics() {
|
||||
writeln!(
|
||||
printer,
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
diagnostic.message().bold()
|
||||
)?;
|
||||
}
|
||||
|
||||
// Write the resolved dependencies to the output channel.
|
||||
let mut writer: Box<dyn std::io::Write> = if let Some(output_file) = output_file {
|
||||
Box::new(AutoStream::auto(fs::File::create(output_file)?))
|
||||
} else {
|
||||
|
|
|
@ -18,7 +18,8 @@ use puffin_dispatch::BuildDispatch;
|
|||
use puffin_installer::{Downloader, InstallPlan, Reinstall, SitePackages};
|
||||
use puffin_interpreter::Virtualenv;
|
||||
use puffin_resolver::{
|
||||
Graph, Manifest, PreReleaseMode, Resolution, ResolutionMode, ResolutionOptions, Resolver,
|
||||
Manifest, PreReleaseMode, ResolutionGraph, ResolutionManifest, ResolutionMode,
|
||||
ResolutionOptions, Resolver,
|
||||
};
|
||||
use puffin_traits::OnceMap;
|
||||
use pypi_types::IndexUrls;
|
||||
|
@ -180,7 +181,7 @@ async fn resolve(
|
|||
cache: &Cache,
|
||||
venv: &Virtualenv,
|
||||
mut printer: Printer,
|
||||
) -> Result<Graph> {
|
||||
) -> Result<ResolutionGraph> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// Create a manifest of the requirements.
|
||||
|
@ -271,7 +272,7 @@ async fn resolve(
|
|||
/// Install a set of requirements into the current environment.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn install(
|
||||
resolution: &Resolution,
|
||||
resolution: &ResolutionManifest,
|
||||
reinstall: &Reinstall,
|
||||
link_mode: LinkMode,
|
||||
index_urls: IndexUrls,
|
||||
|
@ -455,7 +456,11 @@ async fn install(
|
|||
}
|
||||
|
||||
/// Validate the installed packages in the virtual environment.
|
||||
fn validate(resolution: &Resolution, venv: &Virtualenv, mut printer: Printer) -> Result<()> {
|
||||
fn validate(
|
||||
resolution: &ResolutionManifest,
|
||||
venv: &Virtualenv,
|
||||
mut printer: Printer,
|
||||
) -> Result<()> {
|
||||
let site_packages = SitePackages::from_executable(venv)?;
|
||||
let diagnostics = site_packages.diagnostics()?;
|
||||
for diagnostic in diagnostics {
|
||||
|
|
|
@ -1720,3 +1720,103 @@ fn override_multi_dependency() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request an extra that doesn't exist on the specified package.
|
||||
#[test]
|
||||
fn missing_registry_extra() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let cache_dir = TempDir::new()?;
|
||||
let venv = create_venv_py312(&temp_dir, &cache_dir);
|
||||
|
||||
let requirements_in = temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("black[tensorboard]==23.10.1")?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec()
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("pip-compile")
|
||||
.arg("requirements.in")
|
||||
.arg("--cache-dir")
|
||||
.arg(cache_dir.path())
|
||||
.arg("--exclude-newer")
|
||||
.arg(EXCLUDE_NEWER)
|
||||
.env("VIRTUAL_ENV", venv.as_os_str())
|
||||
.current_dir(&temp_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by Puffin v0.0.1 via the following command:
|
||||
# puffin pip-compile requirements.in --cache-dir [CACHE_DIR]
|
||||
black==23.10.1
|
||||
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]
|
||||
warning: The package `black==23.10.1` does not have an extra named `tensorboard`.
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request an extra that doesn't exist on the specified package.
|
||||
#[test]
|
||||
fn missing_url_extra() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let cache_dir = TempDir::new()?;
|
||||
let venv = create_venv_py312(&temp_dir, &cache_dir);
|
||||
|
||||
let requirements_in = temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("flask[tensorboard] @ https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl")?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec()
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.arg("pip-compile")
|
||||
.arg("requirements.in")
|
||||
.arg("--cache-dir")
|
||||
.arg(cache_dir.path())
|
||||
.arg("--exclude-newer")
|
||||
.arg(EXCLUDE_NEWER)
|
||||
.env("VIRTUAL_ENV", venv.as_os_str())
|
||||
.current_dir(&temp_dir), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by Puffin v0.0.1 via the following command:
|
||||
# puffin pip-compile requirements.in --cache-dir [CACHE_DIR]
|
||||
blinker==1.7.0
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
flask @ https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
jinja2==3.1.2
|
||||
# via flask
|
||||
markupsafe==2.1.3
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==3.0.1
|
||||
# via flask
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
warning: The package `flask @ https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl` does not have an extra named `tensorboard`.
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ use puffin_normalize::PackageName;
|
|||
use pypi_types::IndexUrl;
|
||||
|
||||
use crate::error::ResolveError;
|
||||
use crate::resolution::Resolution;
|
||||
use crate::resolution::ResolutionManifest;
|
||||
|
||||
pub struct DistFinder<'a> {
|
||||
tags: &'a Tags,
|
||||
|
@ -48,9 +48,12 @@ impl<'a> DistFinder<'a> {
|
|||
}
|
||||
|
||||
/// Resolve a set of pinned packages into a set of wheels.
|
||||
pub async fn resolve(&self, requirements: &[Requirement]) -> Result<Resolution, ResolveError> {
|
||||
pub async fn resolve(
|
||||
&self,
|
||||
requirements: &[Requirement],
|
||||
) -> Result<ResolutionManifest, ResolveError> {
|
||||
if requirements.is_empty() {
|
||||
return Ok(Resolution::default());
|
||||
return Ok(ResolutionManifest::default());
|
||||
}
|
||||
|
||||
// A channel to fetch package metadata (e.g., given `flask`, fetch all versions).
|
||||
|
@ -96,7 +99,7 @@ impl<'a> DistFinder<'a> {
|
|||
if let Some(reporter) = self.reporter.as_ref() {
|
||||
reporter.on_complete();
|
||||
}
|
||||
return Ok(Resolution::new(resolution));
|
||||
return Ok(ResolutionManifest::new(resolution));
|
||||
}
|
||||
|
||||
// Otherwise, wait for the package stream to complete.
|
||||
|
@ -130,7 +133,7 @@ impl<'a> DistFinder<'a> {
|
|||
reporter.on_complete();
|
||||
}
|
||||
|
||||
Ok(Resolution::new(resolution))
|
||||
Ok(ResolutionManifest::new(resolution))
|
||||
}
|
||||
|
||||
/// select a version that satisfies the requirement, preferring wheels to source distributions.
|
||||
|
|
|
@ -3,7 +3,7 @@ pub use finder::{DistFinder, Reporter as FinderReporter};
|
|||
pub use manifest::Manifest;
|
||||
pub use prerelease_mode::PreReleaseMode;
|
||||
pub use pubgrub::PubGrubReportFormatter;
|
||||
pub use resolution::{Graph, Resolution};
|
||||
pub use resolution::{ResolutionGraph, ResolutionManifest};
|
||||
pub use resolution_mode::ResolutionMode;
|
||||
pub use resolution_options::ResolutionOptions;
|
||||
pub use resolver::{
|
||||
|
|
38
crates/puffin-resolver/src/pubgrub/distribution.rs
Normal file
38
crates/puffin-resolver/src/pubgrub/distribution.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use url::Url;
|
||||
|
||||
use distribution_types::{Metadata, VersionOrUrl};
|
||||
use puffin_normalize::PackageName;
|
||||
|
||||
use crate::pubgrub::PubGrubVersion;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum PubGrubDistribution<'a> {
|
||||
Registry(&'a PackageName, &'a PubGrubVersion),
|
||||
Url(&'a PackageName, &'a Url),
|
||||
}
|
||||
|
||||
impl<'a> PubGrubDistribution<'a> {
|
||||
pub(crate) fn from_registry(name: &'a PackageName, version: &'a PubGrubVersion) -> Self {
|
||||
Self::Registry(name, version)
|
||||
}
|
||||
|
||||
pub(crate) fn from_url(name: &'a PackageName, url: &'a Url) -> Self {
|
||||
Self::Url(name, url)
|
||||
}
|
||||
}
|
||||
|
||||
impl Metadata for PubGrubDistribution<'_> {
|
||||
fn name(&self) -> &PackageName {
|
||||
match self {
|
||||
Self::Registry(name, _) => name,
|
||||
Self::Url(name, _) => name,
|
||||
}
|
||||
}
|
||||
|
||||
fn version_or_url(&self) -> VersionOrUrl {
|
||||
match self {
|
||||
Self::Registry(_, version) => VersionOrUrl::Version((*version).into()),
|
||||
Self::Url(_, url) => VersionOrUrl::Url(url),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
pub(crate) use crate::pubgrub::dependencies::PubGrubDependencies;
|
||||
pub(crate) use crate::pubgrub::distribution::PubGrubDistribution;
|
||||
pub(crate) use crate::pubgrub::package::PubGrubPackage;
|
||||
pub(crate) use crate::pubgrub::priority::{PubGrubPriorities, PubGrubPriority};
|
||||
pub use crate::pubgrub::report::PubGrubReportFormatter;
|
||||
|
||||
pub(crate) use crate::pubgrub::version::{PubGrubVersion, MIN_VERSION};
|
||||
|
||||
pub(crate) use crate::pubgrub::dependencies::PubGrubDependencies;
|
||||
|
||||
mod dependencies;
|
||||
mod distribution;
|
||||
mod package;
|
||||
mod priority;
|
||||
mod report;
|
||||
|
|
|
@ -14,18 +14,19 @@ use url::Url;
|
|||
use distribution_types::{BuiltDist, Dist, Metadata, SourceDist};
|
||||
use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers};
|
||||
use pep508_rs::{Requirement, VersionOrUrl};
|
||||
use puffin_normalize::PackageName;
|
||||
use puffin_normalize::{ExtraName, PackageName};
|
||||
use puffin_traits::OnceMap;
|
||||
use pypi_types::Metadata21;
|
||||
|
||||
use crate::pins::FilePins;
|
||||
use crate::pubgrub::{PubGrubPackage, PubGrubPriority, PubGrubVersion};
|
||||
use crate::pubgrub::{PubGrubDistribution, PubGrubPackage, PubGrubPriority, PubGrubVersion};
|
||||
use crate::ResolveError;
|
||||
|
||||
/// A set of packages pinned at specific versions.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Resolution(FxHashMap<PackageName, Dist>);
|
||||
pub struct ResolutionManifest(FxHashMap<PackageName, Dist>);
|
||||
|
||||
impl Resolution {
|
||||
impl ResolutionManifest {
|
||||
/// Create a new resolution from the given pinned packages.
|
||||
pub(crate) fn new(packages: FxHashMap<PackageName, Dist>) -> Self {
|
||||
Self(packages)
|
||||
|
@ -69,19 +70,26 @@ impl Resolution {
|
|||
/// A complete resolution graph in which every node represents a pinned package and every edge
|
||||
/// represents a dependency between two pinned packages.
|
||||
#[derive(Debug)]
|
||||
pub struct Graph(pub petgraph::graph::Graph<Dist, Range<PubGrubVersion>, petgraph::Directed>);
|
||||
pub struct ResolutionGraph {
|
||||
/// The underlying graph.
|
||||
petgraph: petgraph::graph::Graph<Dist, Range<PubGrubVersion>, petgraph::Directed>,
|
||||
/// Any diagnostics that were encountered while building the graph.
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
impl ResolutionGraph {
|
||||
/// Create a new graph from the resolved `PubGrub` state.
|
||||
pub(crate) fn from_state(
|
||||
selection: &SelectedDependencies<PubGrubPackage, PubGrubVersion>,
|
||||
pins: &FilePins,
|
||||
distributions: &OnceMap<String, Metadata21>,
|
||||
redirects: &OnceMap<Url, Url>,
|
||||
state: &State<PubGrubPackage, Range<PubGrubVersion>, PubGrubPriority>,
|
||||
) -> Result<Self, ResolveError> {
|
||||
// TODO(charlie): petgraph is a really heavy and unnecessary dependency here. We should
|
||||
// write our own graph, given that our requirements are so simple.
|
||||
let mut graph = petgraph::graph::Graph::with_capacity(selection.len(), selection.len());
|
||||
let mut petgraph = petgraph::graph::Graph::with_capacity(selection.len(), selection.len());
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
// Add every package to the graph.
|
||||
let mut inverse =
|
||||
|
@ -97,7 +105,7 @@ impl Graph {
|
|||
let pinned_package =
|
||||
Dist::from_registry(package_name.clone(), version, file, index);
|
||||
|
||||
let index = graph.add_node(pinned_package);
|
||||
let index = petgraph.add_node(pinned_package);
|
||||
inverse.insert(package_name, index);
|
||||
}
|
||||
PubGrubPackage::Package(package_name, None, Some(url)) => {
|
||||
|
@ -106,9 +114,52 @@ impl Graph {
|
|||
.map_or_else(|| url.clone(), |url| url.value().clone());
|
||||
let pinned_package = Dist::from_url(package_name.clone(), url)?;
|
||||
|
||||
let index = graph.add_node(pinned_package);
|
||||
let index = petgraph.add_node(pinned_package);
|
||||
inverse.insert(package_name, index);
|
||||
}
|
||||
PubGrubPackage::Package(package_name, Some(extra), None) => {
|
||||
// Validate that the `extra` exists.
|
||||
let dist = PubGrubDistribution::from_registry(package_name, version);
|
||||
let entry = distributions
|
||||
.get(&dist.package_id())
|
||||
.expect("Every package should have metadata");
|
||||
let metadata = entry.value();
|
||||
|
||||
if !metadata.provides_extras.contains(extra) {
|
||||
let version = Version::from(version.clone());
|
||||
let (index, file) = pins
|
||||
.get(package_name, &version)
|
||||
.expect("Every package should be pinned")
|
||||
.clone();
|
||||
let pinned_package =
|
||||
Dist::from_registry(package_name.clone(), version, file, index);
|
||||
|
||||
diagnostics.push(Diagnostic::MissingExtra {
|
||||
dist: pinned_package,
|
||||
extra: extra.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
PubGrubPackage::Package(package_name, Some(extra), Some(url)) => {
|
||||
// Validate that the `extra` exists.
|
||||
let dist = PubGrubDistribution::from_url(package_name, url);
|
||||
let entry = distributions
|
||||
.get(&dist.package_id())
|
||||
.expect("Every package should have metadata");
|
||||
let metadata = entry.value();
|
||||
|
||||
if !metadata.provides_extras.contains(extra) {
|
||||
let url = redirects
|
||||
.get(url)
|
||||
.map_or_else(|| url.clone(), |url| url.value().clone());
|
||||
let pinned_package = Dist::from_url(package_name.clone(), url)?;
|
||||
|
||||
diagnostics.push(Diagnostic::MissingExtra {
|
||||
dist: pinned_package,
|
||||
extra: extra.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
@ -134,56 +185,68 @@ impl Graph {
|
|||
if self_version.contains(version) {
|
||||
let self_index = &inverse[self_package];
|
||||
let dependency_index = &inverse[dependency_package];
|
||||
graph.update_edge(*self_index, *dependency_index, dependency_range.clone());
|
||||
petgraph.update_edge(
|
||||
*self_index,
|
||||
*dependency_index,
|
||||
dependency_range.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self(graph))
|
||||
Ok(Self {
|
||||
petgraph,
|
||||
diagnostics,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the number of packages in the graph.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.node_count()
|
||||
self.petgraph.node_count()
|
||||
}
|
||||
|
||||
/// Return `true` if there are no packages in the graph.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.node_count() == 0
|
||||
self.petgraph.node_count() == 0
|
||||
}
|
||||
|
||||
/// Return the set of [`Requirement`]s that this graph represents.
|
||||
pub fn requirements(&self) -> Vec<Requirement> {
|
||||
// Collect and sort all packages.
|
||||
let mut nodes = self
|
||||
.0
|
||||
.petgraph
|
||||
.node_indices()
|
||||
.map(|node| (node, &self.0[node]))
|
||||
.map(|node| (node, &self.petgraph[node]))
|
||||
.collect::<Vec<_>>();
|
||||
nodes.sort_unstable_by_key(|(_, package)| package.name());
|
||||
self.0
|
||||
self.petgraph
|
||||
.node_indices()
|
||||
.map(|node| as_requirement(&self.0[node]))
|
||||
.map(|node| as_requirement(&self.petgraph[node]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return the [`Diagnostic`]s that were encountered while building the graph.
|
||||
pub fn diagnostics(&self) -> &[Diagnostic] {
|
||||
&self.diagnostics
|
||||
}
|
||||
|
||||
/// Return the underlying graph.
|
||||
pub fn petgraph(
|
||||
&self,
|
||||
) -> &petgraph::graph::Graph<Dist, Range<PubGrubVersion>, petgraph::Directed> {
|
||||
&self.0
|
||||
&self.petgraph
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the graph in the `{name}=={version}` format of requirements.txt that pip uses.
|
||||
impl std::fmt::Display for Graph {
|
||||
impl std::fmt::Display for ResolutionGraph {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// Collect and sort all packages.
|
||||
let mut nodes = self
|
||||
.0
|
||||
.petgraph
|
||||
.node_indices()
|
||||
.map(|node| (node, &self.0[node]))
|
||||
.map(|node| (node, &self.petgraph[node]))
|
||||
.collect::<Vec<_>>();
|
||||
nodes.sort_unstable_by_key(|(_, package)| package.name());
|
||||
|
||||
|
@ -192,9 +255,9 @@ impl std::fmt::Display for Graph {
|
|||
writeln!(f, "{package}")?;
|
||||
|
||||
let mut edges = self
|
||||
.0
|
||||
.petgraph
|
||||
.edges_directed(index, Direction::Incoming)
|
||||
.map(|edge| &self.0[edge.source()])
|
||||
.map(|edge| &self.petgraph[edge.source()])
|
||||
.collect::<Vec<_>>();
|
||||
edges.sort_unstable_by_key(|package| package.name());
|
||||
|
||||
|
@ -218,13 +281,18 @@ impl std::fmt::Display for Graph {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Graph> for Resolution {
|
||||
fn from(graph: Graph) -> Self {
|
||||
impl From<ResolutionGraph> for ResolutionManifest {
|
||||
fn from(graph: ResolutionGraph) -> Self {
|
||||
Self(
|
||||
graph
|
||||
.0
|
||||
.petgraph
|
||||
.node_indices()
|
||||
.map(|node| (graph.0[node].name().clone(), graph.0[node].clone()))
|
||||
.map(|node| {
|
||||
(
|
||||
graph.petgraph[node].name().clone(),
|
||||
graph.petgraph[node].clone(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
@ -281,3 +349,32 @@ fn as_requirement(dist: &Dist) -> Requirement {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Diagnostic {
|
||||
MissingExtra {
|
||||
/// The distribution that was requested with an non-existent extra. For example,
|
||||
/// `black==23.10.0`.
|
||||
dist: Dist,
|
||||
/// The extra that was requested. For example, `colorama` in `black[colorama]`.
|
||||
extra: ExtraName,
|
||||
},
|
||||
}
|
||||
|
||||
impl Diagnostic {
|
||||
/// Convert the diagnostic into a user-facing message.
|
||||
pub fn message(&self) -> String {
|
||||
match self {
|
||||
Self::MissingExtra { dist, extra } => {
|
||||
format!("The package `{dist}` does not have an extra named `{extra}`.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`PackageName`] is involved in this diagnostic.
|
||||
pub fn includes(&self, name: &PackageName) -> bool {
|
||||
match self {
|
||||
Self::MissingExtra { dist, .. } => name == dist.name(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,10 @@ use crate::manifest::Manifest;
|
|||
use crate::overrides::Overrides;
|
||||
use crate::pins::FilePins;
|
||||
use crate::pubgrub::{
|
||||
PubGrubDependencies, PubGrubPackage, PubGrubPriorities, PubGrubVersion, MIN_VERSION,
|
||||
PubGrubDependencies, PubGrubDistribution, PubGrubPackage, PubGrubPriorities, PubGrubVersion,
|
||||
MIN_VERSION,
|
||||
};
|
||||
use crate::resolution::Graph;
|
||||
use crate::resolution::ResolutionGraph;
|
||||
use crate::version_map::VersionMap;
|
||||
use crate::yanks::AllowedYanks;
|
||||
use crate::ResolutionOptions;
|
||||
|
@ -230,7 +231,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
}
|
||||
|
||||
/// Resolve a set of requirements into a set of pinned versions.
|
||||
pub async fn resolve(self) -> Result<Graph, ResolveError> {
|
||||
pub async fn resolve(self) -> Result<ResolutionGraph, ResolveError> {
|
||||
// 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).
|
||||
let (request_sink, request_stream) = futures::channel::mpsc::unbounded();
|
||||
|
@ -271,7 +272,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
async fn solve(
|
||||
&self,
|
||||
request_sink: &futures::channel::mpsc::UnboundedSender<Request>,
|
||||
) -> Result<Graph, ResolveError> {
|
||||
) -> Result<ResolutionGraph, ResolveError> {
|
||||
let root = PubGrubPackage::Root(self.project.clone());
|
||||
|
||||
// Keep track of the packages for which we've requested metadata.
|
||||
|
@ -301,7 +302,13 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
})
|
||||
else {
|
||||
let selection = state.partial_solution.extract_solution();
|
||||
return Graph::from_state(&selection, &pins, &self.index.redirects, &state);
|
||||
return ResolutionGraph::from_state(
|
||||
&selection,
|
||||
&pins,
|
||||
&self.index.distributions,
|
||||
&self.index.redirects,
|
||||
&state,
|
||||
);
|
||||
};
|
||||
next = highest_priority_pkg;
|
||||
|
||||
|
@ -350,13 +357,6 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
.get_dependencies(package, &version, &mut priorities, &index, request_sink)
|
||||
.await?
|
||||
{
|
||||
Dependencies::Unknown => {
|
||||
state.add_incompatibility(Incompatibility::unavailable_dependencies(
|
||||
package.clone(),
|
||||
version.clone(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
Dependencies::Unusable(reason) => {
|
||||
state.add_incompatibility(Incompatibility::unusable_dependencies(
|
||||
package.clone(),
|
||||
|
@ -639,14 +639,7 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
|
|||
Self::visit_package(package, priorities, index, request_sink)?;
|
||||
}
|
||||
|
||||
if let Some(extra) = extra {
|
||||
if !metadata
|
||||
.provides_extras
|
||||
.iter()
|
||||
.any(|provided_extra| provided_extra == extra)
|
||||
{
|
||||
return Ok(Dependencies::Unknown);
|
||||
}
|
||||
if extra.is_some() {
|
||||
constraints.insert(
|
||||
PubGrubPackage::Package(package_name.clone(), None, None),
|
||||
Range::singleton(version.clone()),
|
||||
|
@ -854,42 +847,8 @@ impl<'a> FromIterator<&'a Url> for AllowedUrls {
|
|||
/// For each [Package] there is a set of versions allowed as a dependency.
|
||||
#[derive(Clone)]
|
||||
enum Dependencies {
|
||||
/// Package dependencies are unavailable.
|
||||
Unknown,
|
||||
/// Package dependencies are not usable
|
||||
Unusable(Option<String>),
|
||||
/// Container for all available package versions.
|
||||
Known(DependencyConstraints<PubGrubPackage, Range<PubGrubVersion>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PubGrubDistribution<'a> {
|
||||
Registry(&'a PackageName, &'a PubGrubVersion),
|
||||
Url(&'a PackageName, &'a Url),
|
||||
}
|
||||
|
||||
impl<'a> PubGrubDistribution<'a> {
|
||||
fn from_registry(name: &'a PackageName, version: &'a PubGrubVersion) -> Self {
|
||||
Self::Registry(name, version)
|
||||
}
|
||||
|
||||
fn from_url(name: &'a PackageName, url: &'a Url) -> Self {
|
||||
Self::Url(name, url)
|
||||
}
|
||||
}
|
||||
|
||||
impl Metadata for PubGrubDistribution<'_> {
|
||||
fn name(&self) -> &PackageName {
|
||||
match self {
|
||||
Self::Registry(name, _) => name,
|
||||
Self::Url(name, _) => name,
|
||||
}
|
||||
}
|
||||
|
||||
fn version_or_url(&self) -> VersionOrUrl {
|
||||
match self {
|
||||
Self::Registry(_, version) => VersionOrUrl::Version((*version).into()),
|
||||
Self::Url(_, url) => VersionOrUrl::Url(url),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ use puffin_cache::Cache;
|
|||
use puffin_client::RegistryClientBuilder;
|
||||
use puffin_interpreter::{Interpreter, Virtualenv};
|
||||
use puffin_resolver::{
|
||||
Graph, Manifest, PreReleaseMode, ResolutionMode, ResolutionOptions, Resolver,
|
||||
Manifest, PreReleaseMode, ResolutionGraph, ResolutionMode, ResolutionOptions, Resolver,
|
||||
};
|
||||
use puffin_traits::{BuildContext, SourceBuildTrait};
|
||||
|
||||
|
@ -97,7 +97,7 @@ async fn resolve(
|
|||
options: ResolutionOptions,
|
||||
markers: &'static MarkerEnvironment,
|
||||
tags: &Tags,
|
||||
) -> Result<Graph> {
|
||||
) -> Result<ResolutionGraph> {
|
||||
let client = RegistryClientBuilder::new(Cache::temp()?).build();
|
||||
let build_context = DummyContext {
|
||||
cache: Cache::temp()?,
|
||||
|
@ -161,6 +161,31 @@ async fn black_colorama() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve Black with an invalid extra. The resolver should ignore the extra.
|
||||
#[tokio::test]
|
||||
async fn black_tensorboard() -> Result<()> {
|
||||
colored::control::set_override(false);
|
||||
|
||||
let manifest = Manifest::new(
|
||||
vec![Requirement::from_str("black[tensorboard]<=23.9.1").unwrap()],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
);
|
||||
let options = ResolutionOptions::new(
|
||||
ResolutionMode::default(),
|
||||
PreReleaseMode::default(),
|
||||
Some(*EXCLUDE_NEWER),
|
||||
);
|
||||
|
||||
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
|
||||
|
||||
insta::assert_display_snapshot!(resolution);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn black_python_310() -> Result<()> {
|
||||
colored::control::set_override(false);
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/puffin-resolver/tests/resolver.rs
|
||||
expression: resolution
|
||||
---
|
||||
black==23.9.1
|
||||
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
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue