mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Hint at tool.uv.environments
on resolution error (#13455)
Users are not (yet) properly familiar with the concept of universal resolution and its implication that we need to resolve for all possible platforms and Python versions. Some projects only target a specific platform or Python version, and users experience resolution errors due to failures for other platforms. Indicated by the number of questions we get about it, `tool.uv.environments` for restricting environments is not well discoverable. We add a special hint when resolution failed on a fork disjoint with the current environment, hinting the user to constrain `requires-python` and `tool.uv.environments` respectively. The hint has false positives for cases where the resolution failed on a different platform, but equally fails on the current platform, in cases where the non-current fork was tried earlier. Given that conflicts can be based on `requires-python`, afaik we can't parse whether the current platform would also be affected from the derivation tree. Two cases not covered by this are build errors as well as install errors that need `tool.uv.required-environments`.
This commit is contained in:
parent
8a88ab2c70
commit
0109af1aa5
14 changed files with 220 additions and 4 deletions
|
@ -206,6 +206,7 @@ mod resolver {
|
|||
options,
|
||||
&python_requirement,
|
||||
markers,
|
||||
interpreter.markers(),
|
||||
conflicts,
|
||||
Some(&TAGS),
|
||||
&flat_index,
|
||||
|
|
|
@ -226,6 +226,7 @@ impl BuildContext for BuildDispatch<'_> {
|
|||
.build(),
|
||||
&python_requirement,
|
||||
ResolverEnvironment::specific(marker_env),
|
||||
self.interpreter.markers(),
|
||||
// Conflicting groups only make sense when doing universal resolution.
|
||||
Conflicts::empty(),
|
||||
Some(tags),
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::fmt::Formatter;
|
|||
use std::sync::Arc;
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use owo_colors::OwoColorize;
|
||||
use pubgrub::{
|
||||
DefaultStringReporter, DerivationTree, Derived, External, Range, Ranges, Reporter, Term,
|
||||
};
|
||||
|
@ -13,7 +14,8 @@ use uv_distribution_types::{
|
|||
DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist,
|
||||
};
|
||||
use uv_normalize::{ExtraName, InvalidNameError, PackageName};
|
||||
use uv_pep440::{LocalVersionSlice, LowerBound, Version};
|
||||
use uv_pep440::{LocalVersionSlice, LowerBound, Version, VersionSpecifier};
|
||||
use uv_pep508::{MarkerEnvironment, MarkerExpression, MarkerTree, MarkerValueVersion};
|
||||
use uv_platform_tags::Tags;
|
||||
use uv_static::EnvVars;
|
||||
|
||||
|
@ -163,6 +165,7 @@ pub struct NoSolutionError {
|
|||
fork_urls: ForkUrls,
|
||||
fork_indexes: ForkIndexes,
|
||||
env: ResolverEnvironment,
|
||||
current_environment: MarkerEnvironment,
|
||||
tags: Option<Tags>,
|
||||
workspace_members: BTreeSet<PackageName>,
|
||||
options: Options,
|
||||
|
@ -184,6 +187,7 @@ impl NoSolutionError {
|
|||
fork_urls: ForkUrls,
|
||||
fork_indexes: ForkIndexes,
|
||||
env: ResolverEnvironment,
|
||||
current_environment: MarkerEnvironment,
|
||||
tags: Option<Tags>,
|
||||
workspace_members: BTreeSet<PackageName>,
|
||||
options: Options,
|
||||
|
@ -202,6 +206,7 @@ impl NoSolutionError {
|
|||
fork_urls,
|
||||
fork_indexes,
|
||||
env,
|
||||
current_environment,
|
||||
tags,
|
||||
workspace_members,
|
||||
options,
|
||||
|
@ -353,6 +358,44 @@ impl NoSolutionError {
|
|||
pub fn header(&self) -> NoSolutionHeader {
|
||||
NoSolutionHeader::new(self.env.clone())
|
||||
}
|
||||
|
||||
/// Hint at limiting the resolver environment if universal resolution failed for a target
|
||||
/// that is not the current platform or not the current Python version.
|
||||
fn hint_disjoint_targets(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||
// Only applicable to universal resolution.
|
||||
let Some(markers) = self.env.fork_markers() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// TODO(konsti): This is a crude approximation to telling the user the difference
|
||||
// between their Python version and the relevant Python version range from the marker.
|
||||
let current_python_version = self.current_environment.python_version().version.clone();
|
||||
let current_python_marker = MarkerTree::expression(MarkerExpression::Version {
|
||||
key: MarkerValueVersion::PythonVersion,
|
||||
specifier: VersionSpecifier::equals_version(current_python_version.clone()),
|
||||
});
|
||||
if markers.is_disjoint(current_python_marker) {
|
||||
write!(
|
||||
f,
|
||||
"\n\n{}{} While the active Python version is {}, \
|
||||
the resolution failed for other Python versions supported by your \
|
||||
project. Consider limiting your project's supported Python versions \
|
||||
using `requires-python`.",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
current_python_version,
|
||||
)?;
|
||||
} else if !markers.evaluate(&self.current_environment, &[]) {
|
||||
write!(
|
||||
f,
|
||||
"\n\n{}{} The resolution failed for an environment that is not the current one, \
|
||||
consider limiting the environments with `tool.uv.environments`.",
|
||||
"hint".bold().cyan(),
|
||||
":".bold(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for NoSolutionError {
|
||||
|
@ -372,6 +415,7 @@ impl std::fmt::Debug for NoSolutionError {
|
|||
fork_urls,
|
||||
fork_indexes,
|
||||
env,
|
||||
current_environment,
|
||||
tags,
|
||||
workspace_members,
|
||||
options,
|
||||
|
@ -389,6 +433,7 @@ impl std::fmt::Debug for NoSolutionError {
|
|||
.field("fork_urls", fork_urls)
|
||||
.field("fork_indexes", fork_indexes)
|
||||
.field("env", env)
|
||||
.field("current_environment", current_environment)
|
||||
.field("tags", tags)
|
||||
.field("workspace_members", workspace_members)
|
||||
.field("options", options)
|
||||
|
@ -473,6 +518,8 @@ impl std::fmt::Display for NoSolutionError {
|
|||
write!(f, "\n\n{hint}")?;
|
||||
}
|
||||
|
||||
self.hint_disjoint_targets(f)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,6 +198,14 @@ impl ResolverEnvironment {
|
|||
crate::marker::requires_python(pep508_marker)
|
||||
}
|
||||
|
||||
/// For a universal resolution, return the markers of the current fork.
|
||||
pub(crate) fn fork_markers(&self) -> Option<MarkerTree> {
|
||||
match self.kind {
|
||||
Kind::Specific { .. } => None,
|
||||
Kind::Universal { markers, .. } => Some(markers),
|
||||
}
|
||||
}
|
||||
|
||||
/// Narrow this environment given the forking markers.
|
||||
///
|
||||
/// This effectively intersects any markers in this environment with the
|
||||
|
|
|
@ -31,7 +31,9 @@ use uv_distribution_types::{
|
|||
use uv_git::GitResolver;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_pep440::{MIN_VERSION, Version, VersionSpecifiers, release_specifiers_to_ranges};
|
||||
use uv_pep508::{MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString};
|
||||
use uv_pep508::{
|
||||
MarkerEnvironment, MarkerExpression, MarkerOperator, MarkerTree, MarkerValueString,
|
||||
};
|
||||
use uv_platform_tags::Tags;
|
||||
use uv_pypi_types::{ConflictItem, ConflictItemRef, Conflicts, VerbatimParsedUrl};
|
||||
use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider};
|
||||
|
@ -115,6 +117,8 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
|
|||
dependency_mode: DependencyMode,
|
||||
hasher: HashStrategy,
|
||||
env: ResolverEnvironment,
|
||||
// The environment of the current Python interpreter.
|
||||
current_environment: MarkerEnvironment,
|
||||
tags: Option<Tags>,
|
||||
python_requirement: PythonRequirement,
|
||||
conflicts: Conflicts,
|
||||
|
@ -158,6 +162,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
|
|||
options: Options,
|
||||
python_requirement: &'a PythonRequirement,
|
||||
env: ResolverEnvironment,
|
||||
current_environment: &MarkerEnvironment,
|
||||
conflicts: Conflicts,
|
||||
tags: Option<&'a Tags>,
|
||||
flat_index: &'a FlatIndex,
|
||||
|
@ -184,6 +189,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
|
|||
options,
|
||||
hasher,
|
||||
env,
|
||||
current_environment,
|
||||
tags.cloned(),
|
||||
python_requirement,
|
||||
conflicts,
|
||||
|
@ -206,6 +212,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
|
|||
options: Options,
|
||||
hasher: &HashStrategy,
|
||||
env: ResolverEnvironment,
|
||||
current_environment: &MarkerEnvironment,
|
||||
tags: Option<Tags>,
|
||||
python_requirement: &PythonRequirement,
|
||||
conflicts: Conflicts,
|
||||
|
@ -234,6 +241,7 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
|
|||
hasher: hasher.clone(),
|
||||
locations: locations.clone(),
|
||||
env,
|
||||
current_environment: current_environment.clone(),
|
||||
tags,
|
||||
python_requirement: python_requirement.clone(),
|
||||
conflicts,
|
||||
|
@ -354,6 +362,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
state.fork_urls,
|
||||
state.fork_indexes,
|
||||
state.env,
|
||||
self.current_environment.clone(),
|
||||
&visited,
|
||||
));
|
||||
}
|
||||
|
@ -2504,6 +2513,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
fork_urls: ForkUrls,
|
||||
fork_indexes: ForkIndexes,
|
||||
env: ResolverEnvironment,
|
||||
current_environment: MarkerEnvironment,
|
||||
visited: &FxHashSet<PackageName>,
|
||||
) -> ResolveError {
|
||||
err = NoSolutionError::collapse_local_version_segments(NoSolutionError::collapse_proxies(
|
||||
|
@ -2589,6 +2599,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
fork_urls,
|
||||
fork_indexes,
|
||||
env,
|
||||
current_environment,
|
||||
self.tags.clone(),
|
||||
self.workspace_members.clone(),
|
||||
self.options.clone(),
|
||||
|
|
|
@ -517,6 +517,7 @@ pub(crate) async fn pip_compile(
|
|||
tags.as_deref(),
|
||||
resolver_env.clone(),
|
||||
python_requirement,
|
||||
interpreter.markers(),
|
||||
Conflicts::empty(),
|
||||
&client,
|
||||
&flat_index,
|
||||
|
|
|
@ -480,6 +480,7 @@ pub(crate) async fn pip_install(
|
|||
Some(&tags),
|
||||
ResolverEnvironment::specific(marker_env.clone()),
|
||||
python_requirement,
|
||||
interpreter.markers(),
|
||||
Conflicts::empty(),
|
||||
&client,
|
||||
&flat_index,
|
||||
|
|
|
@ -29,6 +29,7 @@ use uv_fs::Simplified;
|
|||
use uv_install_wheel::LinkMode;
|
||||
use uv_installer::{Plan, Planner, Preparer, SitePackages};
|
||||
use uv_normalize::{GroupName, PackageName};
|
||||
use uv_pep508::MarkerEnvironment;
|
||||
use uv_platform_tags::Tags;
|
||||
use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment};
|
||||
use uv_python::{PythonEnvironment, PythonInstallation};
|
||||
|
@ -119,6 +120,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
|||
tags: Option<&Tags>,
|
||||
resolver_env: ResolverEnvironment,
|
||||
python_requirement: PythonRequirement,
|
||||
current_environment: &MarkerEnvironment,
|
||||
conflicts: Conflicts,
|
||||
client: &RegistryClient,
|
||||
flat_index: &FlatIndex,
|
||||
|
@ -303,6 +305,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
|
|||
options,
|
||||
&python_requirement,
|
||||
resolver_env,
|
||||
current_environment,
|
||||
conflicts,
|
||||
tags,
|
||||
flat_index,
|
||||
|
|
|
@ -416,6 +416,7 @@ pub(crate) async fn pip_sync(
|
|||
Some(&tags),
|
||||
ResolverEnvironment::specific(marker_env.clone()),
|
||||
python_requirement,
|
||||
interpreter.markers(),
|
||||
Conflicts::empty(),
|
||||
&client,
|
||||
&flat_index,
|
||||
|
|
|
@ -822,6 +822,7 @@ async fn do_lock(
|
|||
None,
|
||||
resolver_env,
|
||||
python_requirement,
|
||||
interpreter.markers(),
|
||||
conflicts.clone(),
|
||||
&client,
|
||||
&flat_index,
|
||||
|
|
|
@ -1924,6 +1924,7 @@ pub(crate) async fn resolve_environment(
|
|||
Some(tags),
|
||||
ResolverEnvironment::specific(marker_env),
|
||||
python_requirement,
|
||||
interpreter.markers(),
|
||||
Conflicts::empty(),
|
||||
&client,
|
||||
&flat_index,
|
||||
|
@ -2296,6 +2297,7 @@ pub(crate) async fn update_environment(
|
|||
Some(tags),
|
||||
ResolverEnvironment::specific(marker_env.clone()),
|
||||
python_requirement,
|
||||
venv.interpreter().markers(),
|
||||
Conflicts::empty(),
|
||||
&client,
|
||||
&flat_index,
|
||||
|
|
|
@ -3688,6 +3688,8 @@ fn lock_requires_python() -> Result<()> {
|
|||
hint: Pre-releases are available for `pygls` in the requested range (e.g., 2.0.0a4), but pre-releases weren't enabled (try: `--prerelease=allow`)
|
||||
|
||||
hint: The `requires-python` value (>=3.7) includes Python versions that are not supported by your dependencies (e.g., pygls>=1.1.0,<=1.2.1 only supports >=3.7.9, <4). Consider using a more restrictive `requires-python` value (like >=3.7.9, <4).
|
||||
|
||||
hint: While the active Python version is 3.12, the resolution failed for other Python versions supported by your project. Consider limiting your project's supported Python versions using `requires-python`.
|
||||
");
|
||||
|
||||
// Require >=3.7, and allow locking to a version of `pygls` that is compatible (==1.0.1).
|
||||
|
@ -27393,3 +27395,136 @@ fn lock_omit_wheels_exclude_newer() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that we hint if the resolution failed in a different Python version.
|
||||
#[test]
|
||||
fn lock_conflict_for_disjoint_python_version() -> Result<()> {
|
||||
let context = TestContext::new("3.9");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"numpy==1.20.3",
|
||||
"pandas==1.5.3",
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies for split (python_full_version >= '3.11'):
|
||||
╰─▶ Because only numpy{python_full_version >= '3.10'}<=1.26.4 is available and pandas==1.5.3 depends on numpy{python_full_version >= '3.10'}>=1.21.0, we can conclude that pandas==1.5.3 depends on numpy>=1.21.0,<=1.26.4.
|
||||
And because your project depends on numpy==1.20.3 and pandas==1.5.3, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: Pre-releases are available for `numpy` in the requested range (e.g., 2.3.0rc1), but pre-releases weren't enabled (try: `--prerelease=allow`)
|
||||
|
||||
hint: While the active Python version is 3.9, the resolution failed for other Python versions supported by your project. Consider limiting your project's supported Python versions using `requires-python`.
|
||||
");
|
||||
|
||||
// Check that the resolution passes on the restricted Python environment.
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
requires-python = "==3.9.*"
|
||||
dependencies = [
|
||||
"numpy==1.20.3",
|
||||
"pandas==1.5.3",
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that we hint if the resolution failed for a different platform.
|
||||
#[test]
|
||||
fn lock_conflict_for_disjoint_platform() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"numpy>=1.24,<1.26; sys_platform == 'exotic'",
|
||||
"numpy>=1.26",
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies for split (sys_platform == 'exotic'):
|
||||
╰─▶ Because only the following versions of numpy{sys_platform == 'exotic'} are available:
|
||||
numpy{sys_platform == 'exotic'}<=1.24.0
|
||||
numpy{sys_platform == 'exotic'}==1.24.1
|
||||
numpy{sys_platform == 'exotic'}==1.24.2
|
||||
numpy{sys_platform == 'exotic'}==1.24.3
|
||||
numpy{sys_platform == 'exotic'}==1.24.4
|
||||
numpy{sys_platform == 'exotic'}==1.25.0
|
||||
numpy{sys_platform == 'exotic'}==1.25.1
|
||||
numpy{sys_platform == 'exotic'}==1.25.2
|
||||
numpy{sys_platform == 'exotic'}>1.26
|
||||
and your project depends on numpy{sys_platform == 'exotic'}>=1.24,<1.26, we can conclude that your project depends on numpy>=1.24.0,<=1.25.2.
|
||||
And because your project depends on numpy>=1.26, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: The resolution failed for an environment that is not the current one, consider limiting the environments with `tool.uv.environments`.
|
||||
");
|
||||
|
||||
// Check that the resolution passes on the restricted environment.
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"numpy>=1.24,<1.26; sys_platform == 'exotic'",
|
||||
"numpy>=1.26",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
environments = [
|
||||
"sys_platform == 'linux' or sys_platform == 'win32' or sys_platform == 'darwin'"
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.lock(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -826,6 +826,8 @@ fn conflict_in_fork() -> Result<()> {
|
|||
package-a{sys_platform == 'os2'}==1.0.0
|
||||
package-a{sys_platform == 'os2'}>2
|
||||
and your project depends on package-a{sys_platform == 'os2'}<2, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: The resolution failed for an environment that is not the current one, consider limiting the environments with `tool.uv.environments`.
|
||||
"
|
||||
);
|
||||
|
||||
|
|
|
@ -14497,7 +14497,7 @@ fn unsupported_requires_python_dynamic_metadata() -> Result<()> {
|
|||
uv_snapshot!(context.filters(), context
|
||||
.pip_compile()
|
||||
.arg("--universal")
|
||||
.arg("requirements.in"), @r###"
|
||||
.arg("requirements.in"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
@ -14507,7 +14507,9 @@ fn unsupported_requires_python_dynamic_metadata() -> Result<()> {
|
|||
╰─▶ Because source-distribution==0.0.3 requires Python >=3.10 and you require source-distribution{python_full_version >= '3.10'}==0.0.3, we can conclude that your requirements are unsatisfiable.
|
||||
|
||||
hint: The source distribution for `source-distribution` (v0.0.3) does not include static metadata. Generating metadata for this package requires Python >=3.10, but Python 3.8.[X] is installed.
|
||||
"###);
|
||||
|
||||
hint: While the active Python version is 3.8, the resolution failed for other Python versions supported by your project. Consider limiting your project's supported Python versions using `requires-python`.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue