Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions src/lock_file/resolve/pypi.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
cell::RefCell,
collections::HashMap,
collections::{HashMap, HashSet},
iter::once,
ops::Deref,
path::{Path, PathBuf},
Expand Down Expand Up @@ -46,7 +46,7 @@ use uv_pypi_types::{Conflicts, HashAlgorithm, HashDigests};
use uv_requirements::LookaheadResolver;
use uv_resolver::{
AllowedYanks, DefaultResolverProvider, FlatIndex, InMemoryIndex, Manifest, Options, Preference,
PreferenceError, Preferences, PythonRequirement, Resolver, ResolverEnvironment,
PreferenceError, Preferences, PythonRequirement, ResolveError, Resolver, ResolverEnvironment,
};
use uv_types::EmptyInstalledPackages;

Expand Down Expand Up @@ -202,6 +202,60 @@ fn print_overridden_requests(package_requests: &HashMap<uv_normalize::PackageNam
}
}

#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum SolveError {
#[error("failed to resolve pypi dependencies")]
NoSolution {
source: Box<uv_resolver::NoSolutionError>,
#[help]
advice: Option<String>,
},
#[error("failed to resolve pypi dependencies")]
Other(#[from] ResolveError),
}

/// Creates a custom `SolveError` from a `ResolveError`.
/// to add some extra information about locked conda packages
fn create_solve_error(
error: ResolveError,
conda_python_packages: &CondaPythonPackages,
) -> SolveError {
match error {
ResolveError::NoSolution(no_solution) => {
let packages: HashSet<_> = no_solution.packages().collect();
let conflicting_packages: Vec<String> = conda_python_packages
.iter()
.filter_map(|(pypi_name, (_, pypi_identifier))| {
if packages.contains(pypi_name) {
Some(format!(
"{}=={}",
pypi_identifier.name.as_source(),
pypi_identifier.version
))
} else {
None
}
})
.collect();

let advice = if conflicting_packages.is_empty() {
None
} else {
Some(format!(
"The following PyPI packages have been pinned by the conda solve, and this version may be causing a conflict:\n{}",
conflicting_packages.join("\n")
))
};

SolveError::NoSolution {
source: no_solution,
advice,
}
}
_ => SolveError::Other(error),
}
}

#[allow(clippy::too_many_arguments)]
pub async fn resolve_pypi(
context: UvResolutionContext,
Expand Down Expand Up @@ -574,8 +628,7 @@ pub async fn resolve_pypi(
))
.resolve()
.await
.into_diagnostic()
.context("failed to resolve pypi dependencies")?;
.map_err(|e| create_solve_error(e, &conda_python_packages))?;
let resolution = Resolution::from(resolution);

// Print the overridden package requests
Expand Down
33 changes: 33 additions & 0 deletions tests/integration_rust/pypi_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,3 +423,36 @@ async fn test_cross_platform_resolve_with_no_build() {
.join("foo-1.0.0-py2.py3-none-any.whl")
);
}

/// This test checks that the help message is correctly generated when a PyPI package is pinned
/// by the conda solve, which may cause a conflict with the PyPI dependencies.
///
/// We expect there to be a help message that informs the user about the pinned package
#[tokio::test]
#[cfg_attr(not(feature = "slow_integration_tests"), ignore)]
async fn test_pinned_help_message() {
let pixi = PixiControl::from_manifest(
r#"
[workspace]
channels = ["https://prefix.dev/conda-forge"]
name = "deleteme"
platforms = ["linux-64"]
version = "0.1.0"

[dependencies]
python = "3.12.*"
pandas = "*"

[pypi-dependencies]
databricks-sql-connector = ">=4.0.0"
"#,
);
// First, it should fail
let result = pixi.unwrap().update_lock_file().await;
let err = result.err().unwrap();
// Second, it should contain a help message
assert_eq!(
format!("{}", err.help().unwrap()),
"The following PyPI packages have been pinned by the conda solve, and this version may be causing a conflict:\npandas==2.3.1"
);
}
Loading