|
1 |
| -use std::fmt::Write; |
2 |
| -use std::{collections::BTreeSet, ffi::OsString}; |
3 |
| - |
4 | 1 | use anyhow::{bail, Context};
|
5 | 2 | use itertools::Itertools;
|
6 | 3 | use owo_colors::OwoColorize;
|
| 4 | +use std::collections::Bound; |
| 5 | +use std::fmt::Write; |
| 6 | +use std::{collections::BTreeSet, ffi::OsString}; |
7 | 7 | use tracing::{debug, warn};
|
8 |
| - |
| 8 | +use uv_cache::Cache; |
| 9 | +use uv_client::BaseClientBuilder; |
9 | 10 | use uv_distribution_types::{InstalledDist, Name};
|
10 | 11 | #[cfg(unix)]
|
11 | 12 | use uv_fs::replace_symlink;
|
12 | 13 | use uv_fs::Simplified;
|
13 | 14 | use uv_installer::SitePackages;
|
| 15 | +use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers}; |
14 | 16 | use uv_pep508::PackageName;
|
15 | 17 | use uv_pypi_types::Requirement;
|
16 |
| -use uv_python::PythonEnvironment; |
17 |
| -use uv_settings::ToolOptions; |
| 18 | +use uv_python::{ |
| 19 | + EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation, |
| 20 | + PythonPreference, PythonRequest, PythonVariant, VersionRequest, |
| 21 | +}; |
| 22 | +use uv_settings::{PythonInstallMirrors, ToolOptions}; |
18 | 23 | use uv_shell::Shell;
|
19 | 24 | use uv_tool::{entrypoint_paths, tool_executable_dir, InstalledTools, Tool, ToolEntrypoint};
|
20 | 25 | use uv_warnings::warn_user;
|
21 | 26 |
|
22 |
| -use crate::commands::ExitStatus; |
| 27 | +use crate::commands::project::ProjectError; |
| 28 | +use crate::commands::reporters::PythonDownloadReporter; |
| 29 | +use crate::commands::{pip, ExitStatus}; |
23 | 30 | use crate::printer::Printer;
|
24 | 31 |
|
25 | 32 | /// Return all packages which contain an executable with the given name.
|
@@ -61,6 +68,95 @@ pub(crate) fn remove_entrypoints(tool: &Tool) {
|
61 | 68 | }
|
62 | 69 | }
|
63 | 70 |
|
| 71 | +/// Given a no-solution error and the [`Interpreter`] that was used during the solve, attempt to |
| 72 | +/// discover an alternate [`Interpreter`] that satisfies the `requires-python` constraint. |
| 73 | +pub(crate) async fn refine_interpreter( |
| 74 | + interpreter: &Interpreter, |
| 75 | + python_request: Option<&PythonRequest>, |
| 76 | + err: &pip::operations::Error, |
| 77 | + client_builder: &BaseClientBuilder<'_>, |
| 78 | + reporter: &PythonDownloadReporter, |
| 79 | + install_mirrors: &PythonInstallMirrors, |
| 80 | + python_preference: PythonPreference, |
| 81 | + python_downloads: PythonDownloads, |
| 82 | + cache: &Cache, |
| 83 | +) -> anyhow::Result<Option<Interpreter>, ProjectError> { |
| 84 | + let pip::operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(ref no_solution_err)) = |
| 85 | + err |
| 86 | + else { |
| 87 | + return Ok(None); |
| 88 | + }; |
| 89 | + |
| 90 | + // Infer the `requires-python` constraint from the error. |
| 91 | + let requires_python = no_solution_err.find_requires_python(); |
| 92 | + |
| 93 | + // If the existing interpreter already satisfies the `requires-python` constraint, we don't need |
| 94 | + // to refine it. We'd expect to fail again anyway. |
| 95 | + if requires_python.contains(interpreter.python_version()) { |
| 96 | + return Ok(None); |
| 97 | + } |
| 98 | + |
| 99 | + // If the user passed a `--python` request, and the refined interpreter is incompatible, we |
| 100 | + // can't use it. |
| 101 | + if let Some(python_request) = python_request { |
| 102 | + if !python_request.satisfied(interpreter, cache) { |
| 103 | + return Ok(None); |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + // We want an interpreter that's as close to the required version as possible. If we choose the |
| 108 | + // "latest" Python, we risk choosing a version that lacks wheels for the tool's requirements |
| 109 | + // (assuming those requirements don't publish source distributions). |
| 110 | + // |
| 111 | + // TODO(charlie): Solve for the Python version iteratively (or even, within the resolver |
| 112 | + // itself). The current strategy can also fail if the tool's requirements have greater |
| 113 | + // `requires-python` constraints, and we didn't see them in the initial solve. It can also fail |
| 114 | + // if the tool's requirements don't publish wheels for this interpreter version, though that's |
| 115 | + // rarer. |
| 116 | + let lower_bound = match requires_python.as_ref() { |
| 117 | + Bound::Included(version) => VersionSpecifier::greater_than_equal_version(version.clone()), |
| 118 | + Bound::Excluded(version) => VersionSpecifier::greater_than_version(version.clone()), |
| 119 | + Bound::Unbounded => unreachable!("`requires-python` should never be unbounded"), |
| 120 | + }; |
| 121 | + |
| 122 | + let upper_bound = match requires_python.as_ref() { |
| 123 | + Bound::Included(version) => { |
| 124 | + let major = version.release().first().copied().unwrap_or(0); |
| 125 | + let minor = version.release().get(1).copied().unwrap_or(0); |
| 126 | + VersionSpecifier::less_than_version(Version::new([major, minor + 1])) |
| 127 | + } |
| 128 | + Bound::Excluded(version) => { |
| 129 | + let major = version.release().first().copied().unwrap_or(0); |
| 130 | + let minor = version.release().get(1).copied().unwrap_or(0); |
| 131 | + VersionSpecifier::less_than_version(Version::new([major, minor + 1])) |
| 132 | + } |
| 133 | + Bound::Unbounded => unreachable!("`requires-python` should never be unbounded"), |
| 134 | + }; |
| 135 | + |
| 136 | + let python_request = PythonRequest::Version(VersionRequest::Range( |
| 137 | + VersionSpecifiers::from_iter([lower_bound, upper_bound]), |
| 138 | + PythonVariant::default(), |
| 139 | + )); |
| 140 | + |
| 141 | + debug!("Refining interpreter with: {python_request}"); |
| 142 | + |
| 143 | + let interpreter = PythonInstallation::find_or_download( |
| 144 | + Some(&python_request), |
| 145 | + EnvironmentPreference::OnlySystem, |
| 146 | + python_preference, |
| 147 | + python_downloads, |
| 148 | + client_builder, |
| 149 | + cache, |
| 150 | + Some(reporter), |
| 151 | + install_mirrors.python_install_mirror.as_deref(), |
| 152 | + install_mirrors.pypy_install_mirror.as_deref(), |
| 153 | + ) |
| 154 | + .await? |
| 155 | + .into_interpreter(); |
| 156 | + |
| 157 | + Ok(Some(interpreter)) |
| 158 | +} |
| 159 | + |
64 | 160 | /// Installs tool executables for a given package and handles any conflicts.
|
65 | 161 | pub(crate) fn install_executables(
|
66 | 162 | environment: &PythonEnvironment,
|
|
0 commit comments