Skip to main content

uv_python/
discovery.rs

1use itertools::{Either, Itertools};
2use owo_colors::AnsiColors;
3use regex::Regex;
4use reqwest_retry::policies::ExponentialBackoff;
5use rustc_hash::{FxBuildHasher, FxHashSet};
6use same_file::is_same_file;
7use std::borrow::Cow;
8use std::env::consts::EXE_SUFFIX;
9use std::fmt::{self, Debug, Formatter};
10use std::{env, io, iter};
11use std::{path::Path, path::PathBuf, str::FromStr};
12use thiserror::Error;
13use tracing::{debug, instrument, trace};
14use uv_cache::Cache;
15use uv_client::BaseClient;
16use uv_distribution_types::RequiresPython;
17use uv_fs::Simplified;
18use uv_fs::which::is_executable;
19use uv_pep440::{
20    LowerBound, Prerelease, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
21    release_specifiers_to_ranges,
22};
23use uv_preview::Preview;
24use uv_static::EnvVars;
25use uv_warnings::anstream;
26use uv_warnings::warn_user_once;
27use which::{which, which_all};
28
29use crate::downloads::{ManagedPythonDownloadList, PlatformRequest, PythonDownloadRequest};
30use crate::implementation::ImplementationName;
31use crate::installation::{PythonInstallation, PythonInstallationKey};
32use crate::interpreter::Error as InterpreterError;
33use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
34use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
35#[cfg(windows)]
36use crate::microsoft_store::find_microsoft_store_pythons;
37use crate::python_version::python_build_versions_from_env;
38use crate::virtualenv::Error as VirtualEnvError;
39use crate::virtualenv::{
40    CondaEnvironmentKind, conda_environment_from_env, virtualenv_from_env,
41    virtualenv_from_working_dir, virtualenv_python_executable,
42};
43#[cfg(windows)]
44use crate::windows_registry::{WindowsPython, registry_pythons};
45use crate::{BrokenLink, Interpreter, PythonVersion};
46
47/// A request to find a Python installation.
48///
49/// See [`PythonRequest::from_str`].
50#[derive(Debug, Clone, Eq, Default)]
51pub enum PythonRequest {
52    /// An appropriate default Python installation
53    ///
54    /// This may skip some Python installations, such as pre-release versions or alternative
55    /// implementations.
56    #[default]
57    Default,
58    /// Any Python installation
59    Any,
60    /// A Python version without an implementation name e.g. `3.10` or `>=3.12,<3.13`
61    Version(VersionRequest),
62    /// A path to a directory containing a Python installation, e.g. `.venv`
63    Directory(PathBuf),
64    /// A path to a Python executable e.g. `~/bin/python`
65    File(PathBuf),
66    /// The name of a Python executable (i.e. for lookup in the PATH) e.g. `foopython3`
67    ExecutableName(String),
68    /// A Python implementation without a version e.g. `pypy` or `pp`
69    Implementation(ImplementationName),
70    /// A Python implementation name and version e.g. `pypy3.8` or `pypy@3.8` or `pp38`
71    ImplementationVersion(ImplementationName, VersionRequest),
72    /// A request for a specific Python installation key e.g. `cpython-3.12-x86_64-linux-gnu`
73    /// Generally these refer to managed Python downloads.
74    Key(PythonDownloadRequest),
75}
76
77impl PartialEq for PythonRequest {
78    fn eq(&self, other: &Self) -> bool {
79        self.to_canonical_string() == other.to_canonical_string()
80    }
81}
82
83impl std::hash::Hash for PythonRequest {
84    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
85        self.to_canonical_string().hash(state);
86    }
87}
88
89impl<'a> serde::Deserialize<'a> for PythonRequest {
90    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91    where
92        D: serde::Deserializer<'a>,
93    {
94        let s = <Cow<'_, str>>::deserialize(deserializer)?;
95        Ok(Self::parse(&s))
96    }
97}
98
99impl serde::Serialize for PythonRequest {
100    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
101    where
102        S: serde::Serializer,
103    {
104        let s = self.to_canonical_string();
105        serializer.serialize_str(&s)
106    }
107}
108
109#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
110#[serde(deny_unknown_fields, rename_all = "kebab-case")]
111#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113pub enum PythonPreference {
114    /// Only use managed Python installations; never use system Python installations.
115    OnlyManaged,
116    #[default]
117    /// Prefer managed Python installations over system Python installations.
118    ///
119    /// System Python installations are still preferred over downloading managed Python versions.
120    /// Use `only-managed` to always fetch a managed Python version.
121    Managed,
122    /// Prefer system Python installations over managed Python installations.
123    ///
124    /// If a system Python installation cannot be found, a managed Python installation can be used.
125    System,
126    /// Only use system Python installations; never use managed Python installations.
127    OnlySystem,
128}
129
130#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
131#[serde(deny_unknown_fields, rename_all = "kebab-case")]
132#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
133#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
134pub enum PythonDownloads {
135    /// Automatically download managed Python installations when needed.
136    #[default]
137    #[serde(alias = "auto")]
138    Automatic,
139    /// Do not automatically download managed Python installations; require explicit installation.
140    Manual,
141    /// Do not ever allow Python downloads.
142    Never,
143}
144
145impl FromStr for PythonDownloads {
146    type Err = String;
147
148    fn from_str(s: &str) -> Result<Self, Self::Err> {
149        match s.to_ascii_lowercase().as_str() {
150            "auto" | "automatic" | "true" | "1" => Ok(Self::Automatic),
151            "manual" => Ok(Self::Manual),
152            "never" | "false" | "0" => Ok(Self::Never),
153            _ => Err(format!("Invalid value for `python-download`: '{s}'")),
154        }
155    }
156}
157
158impl From<bool> for PythonDownloads {
159    fn from(value: bool) -> Self {
160        if value { Self::Automatic } else { Self::Never }
161    }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
165pub enum EnvironmentPreference {
166    /// Only use virtual environments, never allow a system environment.
167    #[default]
168    OnlyVirtual,
169    /// Prefer virtual environments and allow a system environment if explicitly requested.
170    ExplicitSystem,
171    /// Only use a system environment, ignore virtual environments.
172    OnlySystem,
173    /// Allow any environment.
174    Any,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Default)]
178pub(crate) struct DiscoveryPreferences {
179    python_preference: PythonPreference,
180    environment_preference: EnvironmentPreference,
181}
182
183#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
184pub enum PythonVariant {
185    #[default]
186    Default,
187    Debug,
188    Freethreaded,
189    FreethreadedDebug,
190    Gil,
191    GilDebug,
192}
193
194/// A Python discovery version request.
195#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
196pub enum VersionRequest {
197    /// Allow an appropriate default Python version.
198    #[default]
199    Default,
200    /// Allow any Python version.
201    Any,
202    Major(u8, PythonVariant),
203    MajorMinor(u8, u8, PythonVariant),
204    MajorMinorPatch(u8, u8, u8, PythonVariant),
205    MajorMinorPrerelease(u8, u8, Prerelease, PythonVariant),
206    Range(VersionSpecifiers, PythonVariant),
207}
208
209/// The result of an Python installation search.
210///
211/// Returned by [`find_python_installation`].
212type FindPythonResult = Result<PythonInstallation, PythonNotFound>;
213
214/// The result of failed Python installation discovery.
215///
216/// See [`FindPythonResult`].
217#[derive(Clone, Debug, Error)]
218pub struct PythonNotFound {
219    pub request: PythonRequest,
220    pub python_preference: PythonPreference,
221    pub environment_preference: EnvironmentPreference,
222}
223
224/// A location for discovery of a Python installation or interpreter.
225#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)]
226pub enum PythonSource {
227    /// The path was provided directly
228    ProvidedPath,
229    /// An environment was active e.g. via `VIRTUAL_ENV`
230    ActiveEnvironment,
231    /// A conda environment was active e.g. via `CONDA_PREFIX`
232    CondaPrefix,
233    /// A base conda environment was active e.g. via `CONDA_PREFIX`
234    BaseCondaPrefix,
235    /// An environment was discovered e.g. via `.venv`
236    DiscoveredEnvironment,
237    /// An executable was found in the search path i.e. `PATH`
238    SearchPath,
239    /// The first executable found in the search path i.e. `PATH`
240    SearchPathFirst,
241    /// An executable was found in the Windows registry via PEP 514
242    Registry,
243    /// An executable was found in the known Microsoft Store locations
244    MicrosoftStore,
245    /// The Python installation was found in the uv managed Python directory
246    Managed,
247    /// The Python installation was found via the invoking interpreter i.e. via `python -m uv ...`
248    ParentInterpreter,
249}
250
251#[derive(Error, Debug)]
252pub enum Error {
253    #[error(transparent)]
254    Io(#[from] io::Error),
255
256    /// An error was encountering when retrieving interpreter information.
257    #[error("Failed to inspect Python interpreter from {} at `{}` ", _2, _1.user_display())]
258    Query(
259        #[source] Box<crate::interpreter::Error>,
260        PathBuf,
261        PythonSource,
262    ),
263
264    /// An error was encountered while trying to find a managed Python installation matching the
265    /// current platform.
266    #[error("Failed to discover managed Python installations")]
267    ManagedPython(#[from] crate::managed::Error),
268
269    /// An error was encountered when inspecting a virtual environment.
270    #[error(transparent)]
271    VirtualEnv(#[from] crate::virtualenv::Error),
272
273    #[cfg(windows)]
274    #[error("Failed to query installed Python versions from the Windows registry")]
275    RegistryError(#[from] windows::core::Error),
276
277    /// An invalid version request was given
278    #[error("Invalid version request: {0}")]
279    InvalidVersionRequest(String),
280
281    /// The @latest version request was given
282    #[error("Requesting the 'latest' Python version is not yet supported")]
283    LatestVersionRequest,
284
285    // TODO(zanieb): Is this error case necessary still? We should probably drop it.
286    #[error("Interpreter discovery for `{0}` requires `{1}` but only `{2}` is allowed")]
287    SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
288
289    #[error(transparent)]
290    BuildVersion(#[from] crate::python_version::BuildVersionError),
291}
292
293/// Lazily iterate over Python executables in mutable virtual environments.
294///
295/// The following sources are supported:
296///
297/// - Active virtual environment (via `VIRTUAL_ENV`)
298/// - Discovered virtual environment (e.g. `.venv` in a parent directory)
299///
300/// Notably, "system" environments are excluded. See [`python_executables_from_installed`].
301fn python_executables_from_virtual_environments<'a>(
302    preview: Preview,
303) -> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a {
304    let from_active_environment = iter::once_with(|| {
305        virtualenv_from_env()
306            .into_iter()
307            .map(virtualenv_python_executable)
308            .map(|path| Ok((PythonSource::ActiveEnvironment, path)))
309    })
310    .flatten();
311
312    // N.B. we prefer the conda environment over discovered virtual environments
313    let from_conda_environment = iter::once_with(move || {
314        conda_environment_from_env(CondaEnvironmentKind::Child, preview)
315            .into_iter()
316            .map(virtualenv_python_executable)
317            .map(|path| Ok((PythonSource::CondaPrefix, path)))
318    })
319    .flatten();
320
321    let from_discovered_environment = iter::once_with(|| {
322        virtualenv_from_working_dir()
323            .map(|path| {
324                path.map(virtualenv_python_executable)
325                    .map(|path| (PythonSource::DiscoveredEnvironment, path))
326                    .into_iter()
327            })
328            .map_err(Error::from)
329    })
330    .flatten_ok();
331
332    from_active_environment
333        .chain(from_conda_environment)
334        .chain(from_discovered_environment)
335}
336
337/// Lazily iterate over Python executables installed on the system.
338///
339/// The following sources are supported:
340///
341/// - Managed Python installations (e.g. `uv python install`)
342/// - The search path (i.e. `PATH`)
343/// - The registry (Windows only)
344///
345/// The ordering and presence of each source is determined by the [`PythonPreference`].
346///
347/// If a [`VersionRequest`] is provided, we will skip executables that we know do not satisfy the request
348/// and (as discussed in [`python_executables_from_search_path`]) additional version-specific executables may
349/// be included. However, the caller MUST query the returned executables to ensure they satisfy the request;
350/// this function does not guarantee that the executables provide any particular version. See
351/// [`find_python_installation`] instead.
352///
353/// This function does not guarantee that the executables are valid Python interpreters.
354/// See [`python_interpreters_from_executables`].
355fn python_executables_from_installed<'a>(
356    version: &'a VersionRequest,
357    implementation: Option<&'a ImplementationName>,
358    platform: PlatformRequest,
359    preference: PythonPreference,
360) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
361    let from_managed_installations = iter::once_with(move || {
362        ManagedPythonInstallations::from_settings(None)
363            .map_err(Error::from)
364            .and_then(|installed_installations| {
365                debug!(
366                    "Searching for managed installations at `{}`",
367                    installed_installations.root().user_display()
368                );
369                let installations = installed_installations.find_matching_current_platform()?;
370
371                let build_versions = python_build_versions_from_env()?;
372
373                // Check that the Python version and platform satisfy the request to avoid
374                // unnecessary interpreter queries later
375                Ok(installations
376                    .into_iter()
377                    .filter(move |installation| {
378                        if !version.matches_version(&installation.version()) {
379                            debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`");
380                            return false;
381                        }
382                        if !platform.matches(installation.platform()) {
383                            debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
384                            return false;
385                        }
386
387                        if let Some(requested_build) = build_versions.get(&installation.implementation()) {
388                            let Some(installation_build) = installation.build() else {
389                                debug!(
390                                    "Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation"
391                                );
392                                return false;
393                            };
394                            if installation_build != requested_build {
395                                debug!(
396                                    "Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`"
397                                );
398                                return false;
399                            }
400                        }
401
402                        true
403                    })
404                    .inspect(|installation| debug!("Found managed installation `{installation}`"))
405                    .map(move |installation| {
406                        // If it's not a patch version request, then attempt to read the stable
407                        // minor version link.
408                        let executable = version
409                                .patch()
410                                .is_none()
411                                .then(|| {
412                                    PythonMinorVersionLink::from_installation(
413                                        &installation,
414                                    )
415                                    .filter(PythonMinorVersionLink::exists)
416                                    .map(
417                                        |minor_version_link| {
418                                            minor_version_link.symlink_executable.clone()
419                                        },
420                                    )
421                                })
422                                .flatten()
423                                .unwrap_or_else(|| installation.executable(false));
424                        (PythonSource::Managed, executable)
425                    })
426                )
427            })
428    })
429    .flatten_ok();
430
431    let from_search_path = iter::once_with(move || {
432        python_executables_from_search_path(version, implementation)
433            .enumerate()
434            .map(|(i, path)| {
435                if i == 0 {
436                    Ok((PythonSource::SearchPathFirst, path))
437                } else {
438                    Ok((PythonSource::SearchPath, path))
439                }
440            })
441    })
442    .flatten();
443
444    let from_windows_registry = iter::once_with(move || {
445        #[cfg(windows)]
446        {
447            // Skip interpreter probing if we already know the version doesn't match.
448            let version_filter = move |entry: &WindowsPython| {
449                if let Some(found) = &entry.version {
450                    // Some distributions emit the patch version (example: `SysVersion: 3.9`)
451                    if found.string.chars().filter(|c| *c == '.').count() == 1 {
452                        version.matches_major_minor(found.major(), found.minor())
453                    } else {
454                        version.matches_version(found)
455                    }
456                } else {
457                    true
458                }
459            };
460
461            env::var_os(EnvVars::UV_TEST_PYTHON_PATH)
462                .is_none()
463                .then(|| {
464                    registry_pythons()
465                        .map(|entries| {
466                            entries
467                                .into_iter()
468                                .filter(version_filter)
469                                .map(|entry| (PythonSource::Registry, entry.path))
470                                .chain(
471                                    find_microsoft_store_pythons()
472                                        .filter(version_filter)
473                                        .map(|entry| (PythonSource::MicrosoftStore, entry.path)),
474                                )
475                        })
476                        .map_err(Error::from)
477                })
478                .into_iter()
479                .flatten_ok()
480        }
481        #[cfg(not(windows))]
482        {
483            Vec::new()
484        }
485    })
486    .flatten();
487
488    match preference {
489        PythonPreference::OnlyManaged => {
490            // TODO(zanieb): Ideally, we'd create "fake" managed installation directories for tests,
491            // but for now... we'll just include the test interpreters which are always on the
492            // search path.
493            if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
494                Box::new(from_managed_installations.chain(from_search_path))
495            } else {
496                Box::new(from_managed_installations)
497            }
498        }
499        PythonPreference::Managed => Box::new(
500            from_managed_installations
501                .chain(from_search_path)
502                .chain(from_windows_registry),
503        ),
504        PythonPreference::System => Box::new(
505            from_search_path
506                .chain(from_windows_registry)
507                .chain(from_managed_installations),
508        ),
509        PythonPreference::OnlySystem => Box::new(from_search_path.chain(from_windows_registry)),
510    }
511}
512
513/// Lazily iterate over all discoverable Python executables.
514///
515/// Note that Python executables may be excluded by the given [`EnvironmentPreference`],
516/// [`PythonPreference`], and [`PlatformRequest`]. However, these filters are only applied for
517/// performance. We cannot guarantee that the all requests or preferences are satisfied until we
518/// query the interpreter.
519///
520/// See [`python_executables_from_installed`] and [`python_executables_from_virtual_environments`]
521/// for more information on discovery.
522fn python_executables<'a>(
523    version: &'a VersionRequest,
524    implementation: Option<&'a ImplementationName>,
525    platform: PlatformRequest,
526    environments: EnvironmentPreference,
527    preference: PythonPreference,
528    preview: Preview,
529) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
530    // Always read from `UV_INTERNAL__PARENT_INTERPRETER` — it could be a system interpreter
531    let from_parent_interpreter = iter::once_with(|| {
532        env::var_os(EnvVars::UV_INTERNAL__PARENT_INTERPRETER)
533            .into_iter()
534            .map(|path| Ok((PythonSource::ParentInterpreter, PathBuf::from(path))))
535    })
536    .flatten();
537
538    // Check if the base conda environment is active
539    let from_base_conda_environment = iter::once_with(move || {
540        conda_environment_from_env(CondaEnvironmentKind::Base, preview)
541            .into_iter()
542            .map(virtualenv_python_executable)
543            .map(|path| Ok((PythonSource::BaseCondaPrefix, path)))
544    })
545    .flatten();
546
547    let from_virtual_environments = python_executables_from_virtual_environments(preview);
548    let from_installed =
549        python_executables_from_installed(version, implementation, platform, preference);
550
551    // Limit the search to the relevant environment preference; this avoids unnecessary work like
552    // traversal of the file system. Subsequent filtering should be done by the caller with
553    // `source_satisfies_environment_preference` and `interpreter_satisfies_environment_preference`.
554    match environments {
555        EnvironmentPreference::OnlyVirtual => {
556            Box::new(from_parent_interpreter.chain(from_virtual_environments))
557        }
558        EnvironmentPreference::ExplicitSystem | EnvironmentPreference::Any => Box::new(
559            from_parent_interpreter
560                .chain(from_virtual_environments)
561                .chain(from_base_conda_environment)
562                .chain(from_installed),
563        ),
564        EnvironmentPreference::OnlySystem => Box::new(
565            from_parent_interpreter
566                .chain(from_base_conda_environment)
567                .chain(from_installed),
568        ),
569    }
570}
571
572/// Lazily iterate over Python executables in the `PATH`.
573///
574/// The [`VersionRequest`] and [`ImplementationName`] are used to determine the possible
575/// Python interpreter names, e.g. if looking for Python 3.9 we will look for `python3.9`
576/// or if looking for `PyPy` we will look for `pypy` in addition to the default names.
577///
578/// Executables are returned in the search path order, then by specificity of the name, e.g.
579/// `python3.9` is preferred over `python3` and `pypy3.9` is preferred over `python3.9`.
580///
581/// If a `version` is not provided, we will only look for default executable names e.g.
582/// `python3` and `python` — `python3.9` and similar will not be included.
583fn python_executables_from_search_path<'a>(
584    version: &'a VersionRequest,
585    implementation: Option<&'a ImplementationName>,
586) -> impl Iterator<Item = PathBuf> + 'a {
587    // `UV_TEST_PYTHON_PATH` can be used to override `PATH` to limit Python executable availability in the test suite
588    let search_path = env::var_os(EnvVars::UV_TEST_PYTHON_PATH)
589        .unwrap_or(env::var_os(EnvVars::PATH).unwrap_or_default());
590
591    let possible_names: Vec<_> = version
592        .executable_names(implementation)
593        .into_iter()
594        .map(|name| name.to_string())
595        .collect();
596
597    trace!(
598        "Searching PATH for executables: {}",
599        possible_names.join(", ")
600    );
601
602    // Split and iterate over the paths instead of using `which_all` so we can
603    // check multiple names per directory while respecting the search path order and python names
604    // precedence.
605    let search_dirs: Vec<_> = env::split_paths(&search_path).collect();
606    let mut seen_dirs = FxHashSet::with_capacity_and_hasher(search_dirs.len(), FxBuildHasher);
607    search_dirs
608        .into_iter()
609        .filter(|dir| dir.is_dir())
610        .flat_map(move |dir| {
611            // Clone the directory for second closure
612            let dir_clone = dir.clone();
613            trace!(
614                "Checking `PATH` directory for interpreters: {}",
615                dir.display()
616            );
617            same_file::Handle::from_path(&dir)
618                // Skip directories we've already seen, to avoid inspecting interpreters multiple
619                // times when directories are repeated or symlinked in the `PATH`
620                .map(|handle| seen_dirs.insert(handle))
621                .inspect(|fresh_dir| {
622                    if !fresh_dir {
623                        trace!("Skipping already seen directory: {}", dir.display());
624                    }
625                })
626                // If we cannot determine if the directory is unique, we'll assume it is
627                .unwrap_or(true)
628                .then(|| {
629                    possible_names
630                        .clone()
631                        .into_iter()
632                        .flat_map(move |name| {
633                            // Since we're just working with a single directory at a time, we collect to simplify ownership
634                            which::which_in_global(&*name, Some(&dir))
635                                .into_iter()
636                                .flatten()
637                                // We have to collect since `which` requires that the regex outlives its
638                                // parameters, and the dir is local while we return the iterator.
639                                .collect::<Vec<_>>()
640                        })
641                        .chain(find_all_minor(implementation, version, &dir_clone))
642                        .filter(|path| !is_windows_store_shim(path))
643                        .inspect(|path| {
644                            trace!("Found possible Python executable: {}", path.display());
645                        })
646                        .chain(
647                            // TODO(zanieb): Consider moving `python.bat` into `possible_names` to avoid a chain
648                            cfg!(windows)
649                                .then(move || {
650                                    which::which_in_global("python.bat", Some(&dir_clone))
651                                        .into_iter()
652                                        .flatten()
653                                        .collect::<Vec<_>>()
654                                })
655                                .into_iter()
656                                .flatten(),
657                        )
658                })
659                .into_iter()
660                .flatten()
661        })
662}
663
664/// Find all acceptable `python3.x` minor versions.
665///
666/// For example, let's say `python` and `python3` are Python 3.10. When a user requests `>= 3.11`,
667/// we still need to find a `python3.12` in PATH.
668fn find_all_minor(
669    implementation: Option<&ImplementationName>,
670    version_request: &VersionRequest,
671    dir: &Path,
672) -> impl Iterator<Item = PathBuf> + use<> {
673    match version_request {
674        &VersionRequest::Any
675        | VersionRequest::Default
676        | VersionRequest::Major(_, _)
677        | VersionRequest::Range(_, _) => {
678            let regex = if let Some(implementation) = implementation {
679                Regex::new(&format!(
680                    r"^({}|python3)\.(?<minor>\d\d?)t?{}$",
681                    regex::escape(&implementation.to_string()),
682                    regex::escape(EXE_SUFFIX)
683                ))
684                .unwrap()
685            } else {
686                Regex::new(&format!(
687                    r"^python3\.(?<minor>\d\d?)t?{}$",
688                    regex::escape(EXE_SUFFIX)
689                ))
690                .unwrap()
691            };
692            let all_minors = fs_err::read_dir(dir)
693                .into_iter()
694                .flatten()
695                .flatten()
696                .map(|entry| entry.path())
697                .filter(move |path| {
698                    let Some(filename) = path.file_name() else {
699                        return false;
700                    };
701                    let Some(filename) = filename.to_str() else {
702                        return false;
703                    };
704                    let Some(captures) = regex.captures(filename) else {
705                        return false;
706                    };
707
708                    // Filter out interpreter we already know have a too low minor version.
709                    let minor = captures["minor"].parse().ok();
710                    if let Some(minor) = minor {
711                        // Optimization: Skip generally unsupported Python versions without querying.
712                        if minor < 6 {
713                            return false;
714                        }
715                        // Optimization 2: Skip excluded Python (minor) versions without querying.
716                        if !version_request.matches_major_minor(3, minor) {
717                            return false;
718                        }
719                    }
720                    true
721                })
722                .filter(|path| is_executable(path))
723                .collect::<Vec<_>>();
724            Either::Left(all_minors.into_iter())
725        }
726        VersionRequest::MajorMinor(_, _, _)
727        | VersionRequest::MajorMinorPatch(_, _, _, _)
728        | VersionRequest::MajorMinorPrerelease(_, _, _, _) => Either::Right(iter::empty()),
729    }
730}
731
732/// Lazily iterate over all discoverable Python interpreters.
733///
734/// Note interpreters may be excluded by the given [`EnvironmentPreference`], [`PythonPreference`],
735/// [`VersionRequest`], or [`PlatformRequest`].
736///
737/// The [`PlatformRequest`] is currently only applied to managed Python installations before querying
738/// the interpreter. The caller is responsible for ensuring it is applied otherwise.
739///
740/// See [`python_executables`] for more information on discovery.
741fn python_installations<'a>(
742    version: &'a VersionRequest,
743    implementation: Option<&'a ImplementationName>,
744    platform: PlatformRequest,
745    environments: EnvironmentPreference,
746    preference: PythonPreference,
747    cache: &'a Cache,
748    preview: Preview,
749) -> impl Iterator<Item = Result<PythonInstallation, Error>> + 'a {
750    let installations = python_installations_from_executables(
751        // Perform filtering on the discovered executables based on their source. This avoids
752        // unnecessary interpreter queries, which are generally expensive. We'll filter again
753        // with `interpreter_satisfies_environment_preference` after querying.
754        python_executables(
755            version,
756            implementation,
757            platform,
758            environments,
759            preference,
760            preview,
761        )
762        .filter_ok(move |(source, path)| {
763            source_satisfies_environment_preference(*source, path, environments)
764        }),
765        cache,
766    )
767    .filter_ok(move |installation| {
768        interpreter_satisfies_environment_preference(
769            installation.source,
770            &installation.interpreter,
771            environments,
772        )
773    })
774    .filter_ok(move |installation| {
775        let request = version.clone().into_request_for_source(installation.source);
776        if request.matches_interpreter(&installation.interpreter) {
777            true
778        } else {
779            debug!(
780                "Skipping interpreter at `{}` from {}: does not satisfy request `{request}`",
781                installation.interpreter.sys_executable().user_display(),
782                installation.source,
783            );
784            false
785        }
786    })
787    .filter_ok(move |installation| preference.allows_installation(installation));
788
789    if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
790        Either::Left(installations.map_ok(|mut installation| {
791            // In test mode, change the source to `Managed` if a version was marked as such via
792            // `TestContext::with_versions_as_managed`.
793            if installation.interpreter.is_managed() {
794                installation.source = PythonSource::Managed;
795            }
796            installation
797        }))
798    } else {
799        Either::Right(installations)
800    }
801}
802
803/// Lazily convert Python executables into installations.
804fn python_installations_from_executables<'a>(
805    executables: impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
806    cache: &'a Cache,
807) -> impl Iterator<Item = Result<PythonInstallation, Error>> + 'a {
808    executables.map(|result| match result {
809        Ok((source, path)) => Interpreter::query(&path, cache)
810            .map(|interpreter| PythonInstallation {
811                source,
812                interpreter,
813            })
814            .inspect(|installation| {
815                debug!(
816                    "Found `{}` at `{}` ({source})",
817                    installation.key(),
818                    path.display()
819                );
820            })
821            .map_err(|err| Error::Query(Box::new(err), path, source))
822            .inspect_err(|err| debug!("{err}")),
823        Err(err) => Err(err),
824    })
825}
826
827/// Whether a [`Interpreter`] matches the [`EnvironmentPreference`].
828///
829/// This is the correct way to determine if an interpreter matches the preference. In contrast,
830/// [`source_satisfies_environment_preference`] only checks if a [`PythonSource`] **could** satisfy
831/// preference as a pre-filtering step. We cannot definitively know if a Python interpreter is in
832/// a virtual environment until we query it.
833fn interpreter_satisfies_environment_preference(
834    source: PythonSource,
835    interpreter: &Interpreter,
836    preference: EnvironmentPreference,
837) -> bool {
838    match (
839        preference,
840        // Conda environments are not conformant virtual environments but we treat them as such.
841        interpreter.is_virtualenv() || (matches!(source, PythonSource::CondaPrefix)),
842    ) {
843        (EnvironmentPreference::Any, _) => true,
844        (EnvironmentPreference::OnlyVirtual, true) => true,
845        (EnvironmentPreference::OnlyVirtual, false) => {
846            debug!(
847                "Ignoring Python interpreter at `{}`: only virtual environments allowed",
848                interpreter.sys_executable().display()
849            );
850            false
851        }
852        (EnvironmentPreference::ExplicitSystem, true) => true,
853        (EnvironmentPreference::ExplicitSystem, false) => {
854            if matches!(
855                source,
856                PythonSource::ProvidedPath | PythonSource::ParentInterpreter
857            ) {
858                debug!(
859                    "Allowing explicitly requested system Python interpreter at `{}`",
860                    interpreter.sys_executable().display()
861                );
862                true
863            } else {
864                debug!(
865                    "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
866                    interpreter.sys_executable().display()
867                );
868                false
869            }
870        }
871        (EnvironmentPreference::OnlySystem, true) => {
872            debug!(
873                "Ignoring Python interpreter at `{}`: system interpreter required",
874                interpreter.sys_executable().display()
875            );
876            false
877        }
878        (EnvironmentPreference::OnlySystem, false) => true,
879    }
880}
881
882/// Returns true if a [`PythonSource`] could satisfy the [`EnvironmentPreference`].
883///
884/// This is useful as a pre-filtering step. Use of [`interpreter_satisfies_environment_preference`]
885/// is required to determine if an [`Interpreter`] satisfies the preference.
886///
887/// The interpreter path is only used for debug messages.
888fn source_satisfies_environment_preference(
889    source: PythonSource,
890    interpreter_path: &Path,
891    preference: EnvironmentPreference,
892) -> bool {
893    match preference {
894        EnvironmentPreference::Any => true,
895        EnvironmentPreference::OnlyVirtual => {
896            if source.is_maybe_virtualenv() {
897                true
898            } else {
899                debug!(
900                    "Ignoring Python interpreter at `{}`: only virtual environments allowed",
901                    interpreter_path.display()
902                );
903                false
904            }
905        }
906        EnvironmentPreference::ExplicitSystem => {
907            if source.is_maybe_virtualenv() {
908                true
909            } else {
910                debug!(
911                    "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
912                    interpreter_path.display()
913                );
914                false
915            }
916        }
917        EnvironmentPreference::OnlySystem => {
918            if source.is_maybe_system() {
919                true
920            } else {
921                debug!(
922                    "Ignoring Python interpreter at `{}`: system interpreter required",
923                    interpreter_path.display()
924                );
925                false
926            }
927        }
928    }
929}
930
931/// Check if an encountered error is critical and should stop discovery.
932///
933/// Returns false when an error could be due to a faulty Python installation and we should continue searching for a working one.
934impl Error {
935    pub fn is_critical(&self) -> bool {
936        match self {
937            // When querying the Python interpreter fails, we will only raise errors that demonstrate that something is broken
938            // If the Python interpreter returned a bad response, we'll continue searching for one that works
939            Self::Query(err, _, source) => match &**err {
940                InterpreterError::Encode(_)
941                | InterpreterError::Io(_)
942                | InterpreterError::SpawnFailed { .. } => true,
943                InterpreterError::UnexpectedResponse(UnexpectedResponseError { path, .. })
944                | InterpreterError::StatusCode(StatusCodeError { path, .. }) => {
945                    debug!(
946                        "Skipping bad interpreter at {} from {source}: {err}",
947                        path.display()
948                    );
949                    false
950                }
951                InterpreterError::QueryScript { path, err } => {
952                    debug!(
953                        "Skipping bad interpreter at {} from {source}: {err}",
954                        path.display()
955                    );
956                    false
957                }
958                #[cfg(windows)]
959                InterpreterError::CorruptWindowsPackage { path, err } => {
960                    debug!(
961                        "Skipping bad interpreter at {} from {source}: {err}",
962                        path.display()
963                    );
964                    false
965                }
966                InterpreterError::PermissionDenied { path, err } => {
967                    debug!(
968                        "Skipping unexecutable interpreter at {} from {source}: {err}",
969                        path.display()
970                    );
971                    false
972                }
973                InterpreterError::NotFound(path)
974                | InterpreterError::BrokenLink(BrokenLink { path, .. }) => {
975                    // If the interpreter is from an active, valid virtual environment, we should
976                    // fail because it's broken
977                    if matches!(source, PythonSource::ActiveEnvironment)
978                        && uv_fs::is_virtualenv_executable(path)
979                    {
980                        true
981                    } else {
982                        trace!("Skipping missing interpreter at {}", path.display());
983                        false
984                    }
985                }
986            },
987            Self::VirtualEnv(VirtualEnvError::MissingPyVenvCfg(path)) => {
988                trace!("Skipping broken virtualenv at {}", path.display());
989                false
990            }
991            _ => true,
992        }
993    }
994}
995
996/// Create a [`PythonInstallation`] from a Python interpreter path.
997fn python_installation_from_executable(
998    path: &PathBuf,
999    cache: &Cache,
1000) -> Result<PythonInstallation, crate::interpreter::Error> {
1001    Ok(PythonInstallation {
1002        source: PythonSource::ProvidedPath,
1003        interpreter: Interpreter::query(path, cache)?,
1004    })
1005}
1006
1007/// Create a [`PythonInstallation`] from a Python installation root directory.
1008fn python_installation_from_directory(
1009    path: &PathBuf,
1010    cache: &Cache,
1011) -> Result<PythonInstallation, crate::interpreter::Error> {
1012    let executable = virtualenv_python_executable(path);
1013    python_installation_from_executable(&executable, cache)
1014}
1015
1016/// Lazily iterate over all Python installations on the path with the given executable name.
1017fn python_installations_with_executable_name<'a>(
1018    name: &'a str,
1019    cache: &'a Cache,
1020) -> impl Iterator<Item = Result<PythonInstallation, Error>> + 'a {
1021    python_installations_from_executables(
1022        which_all(name)
1023            .into_iter()
1024            .flat_map(|inner| inner.map(|path| Ok((PythonSource::SearchPath, path)))),
1025        cache,
1026    )
1027}
1028
1029/// Iterate over all Python installations that satisfy the given request.
1030pub fn find_python_installations<'a>(
1031    request: &'a PythonRequest,
1032    environments: EnvironmentPreference,
1033    preference: PythonPreference,
1034    cache: &'a Cache,
1035    preview: Preview,
1036) -> Box<dyn Iterator<Item = Result<FindPythonResult, Error>> + 'a> {
1037    let sources = DiscoveryPreferences {
1038        python_preference: preference,
1039        environment_preference: environments,
1040    }
1041    .sources(request);
1042
1043    match request {
1044        PythonRequest::File(path) => Box::new(iter::once({
1045            if preference.allows_source(PythonSource::ProvidedPath) {
1046                debug!("Checking for Python interpreter at {request}");
1047                match python_installation_from_executable(path, cache) {
1048                    Ok(installation) => Ok(Ok(installation)),
1049                    Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1050                        Ok(Err(PythonNotFound {
1051                            request: request.clone(),
1052                            python_preference: preference,
1053                            environment_preference: environments,
1054                        }))
1055                    }
1056                    Err(err) => Err(Error::Query(
1057                        Box::new(err),
1058                        path.clone(),
1059                        PythonSource::ProvidedPath,
1060                    )),
1061                }
1062            } else {
1063                Err(Error::SourceNotAllowed(
1064                    request.clone(),
1065                    PythonSource::ProvidedPath,
1066                    preference,
1067                ))
1068            }
1069        })),
1070        PythonRequest::Directory(path) => Box::new(iter::once({
1071            if preference.allows_source(PythonSource::ProvidedPath) {
1072                debug!("Checking for Python interpreter in {request}");
1073                match python_installation_from_directory(path, cache) {
1074                    Ok(installation) => Ok(Ok(installation)),
1075                    Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1076                        Ok(Err(PythonNotFound {
1077                            request: request.clone(),
1078                            python_preference: preference,
1079                            environment_preference: environments,
1080                        }))
1081                    }
1082                    Err(err) => Err(Error::Query(
1083                        Box::new(err),
1084                        path.clone(),
1085                        PythonSource::ProvidedPath,
1086                    )),
1087                }
1088            } else {
1089                Err(Error::SourceNotAllowed(
1090                    request.clone(),
1091                    PythonSource::ProvidedPath,
1092                    preference,
1093                ))
1094            }
1095        })),
1096        PythonRequest::ExecutableName(name) => {
1097            if preference.allows_source(PythonSource::SearchPath) {
1098                debug!("Searching for Python interpreter with {request}");
1099                Box::new(
1100                    python_installations_with_executable_name(name, cache)
1101                        .filter_ok(move |installation| {
1102                            interpreter_satisfies_environment_preference(
1103                                installation.source,
1104                                &installation.interpreter,
1105                                environments,
1106                            )
1107                        })
1108                        .map_ok(Ok),
1109                )
1110            } else {
1111                Box::new(iter::once(Err(Error::SourceNotAllowed(
1112                    request.clone(),
1113                    PythonSource::SearchPath,
1114                    preference,
1115                ))))
1116            }
1117        }
1118        PythonRequest::Any => Box::new({
1119            debug!("Searching for any Python interpreter in {sources}");
1120            python_installations(
1121                &VersionRequest::Any,
1122                None,
1123                PlatformRequest::default(),
1124                environments,
1125                preference,
1126                cache,
1127                preview,
1128            )
1129            .map_ok(Ok)
1130        }),
1131        PythonRequest::Default => Box::new({
1132            debug!("Searching for default Python interpreter in {sources}");
1133            python_installations(
1134                &VersionRequest::Default,
1135                None,
1136                PlatformRequest::default(),
1137                environments,
1138                preference,
1139                cache,
1140                preview,
1141            )
1142            .map_ok(Ok)
1143        }),
1144        PythonRequest::Version(version) => {
1145            if let Err(err) = version.check_supported() {
1146                return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1147            }
1148            Box::new({
1149                debug!("Searching for {request} in {sources}");
1150                python_installations(
1151                    version,
1152                    None,
1153                    PlatformRequest::default(),
1154                    environments,
1155                    preference,
1156                    cache,
1157                    preview,
1158                )
1159                .map_ok(Ok)
1160            })
1161        }
1162        PythonRequest::Implementation(implementation) => Box::new({
1163            debug!("Searching for a {request} interpreter in {sources}");
1164            python_installations(
1165                &VersionRequest::Default,
1166                Some(implementation),
1167                PlatformRequest::default(),
1168                environments,
1169                preference,
1170                cache,
1171                preview,
1172            )
1173            .filter_ok(|installation| implementation.matches_interpreter(&installation.interpreter))
1174            .map_ok(Ok)
1175        }),
1176        PythonRequest::ImplementationVersion(implementation, version) => {
1177            if let Err(err) = version.check_supported() {
1178                return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1179            }
1180            Box::new({
1181                debug!("Searching for {request} in {sources}");
1182                python_installations(
1183                    version,
1184                    Some(implementation),
1185                    PlatformRequest::default(),
1186                    environments,
1187                    preference,
1188                    cache,
1189                    preview,
1190                )
1191                .filter_ok(|installation| {
1192                    implementation.matches_interpreter(&installation.interpreter)
1193                })
1194                .map_ok(Ok)
1195            })
1196        }
1197        PythonRequest::Key(request) => {
1198            if let Some(version) = request.version() {
1199                if let Err(err) = version.check_supported() {
1200                    return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1201                }
1202            }
1203
1204            Box::new({
1205                debug!("Searching for {request} in {sources}");
1206                python_installations(
1207                    request.version().unwrap_or(&VersionRequest::Default),
1208                    request.implementation(),
1209                    request.platform(),
1210                    environments,
1211                    preference,
1212                    cache,
1213                    preview,
1214                )
1215                .filter_ok(move |installation| {
1216                    request.satisfied_by_interpreter(&installation.interpreter)
1217                })
1218                .map_ok(Ok)
1219            })
1220        }
1221    }
1222}
1223
1224/// Find a Python installation that satisfies the given request.
1225///
1226/// If an error is encountered while locating or inspecting a candidate installation,
1227/// the error will raised instead of attempting further candidates.
1228pub(crate) fn find_python_installation(
1229    request: &PythonRequest,
1230    environments: EnvironmentPreference,
1231    preference: PythonPreference,
1232    cache: &Cache,
1233    preview: Preview,
1234) -> Result<FindPythonResult, Error> {
1235    let installations =
1236        find_python_installations(request, environments, preference, cache, preview);
1237    let mut first_prerelease = None;
1238    let mut first_debug = None;
1239    let mut first_managed = None;
1240    let mut first_error = None;
1241    for result in installations {
1242        // Iterate until the first critical error or happy result
1243        if !result.as_ref().err().is_none_or(Error::is_critical) {
1244            // Track the first non-critical error
1245            if first_error.is_none() {
1246                if let Err(err) = result {
1247                    first_error = Some(err);
1248                }
1249            }
1250            continue;
1251        }
1252
1253        // If it's an error, we're done.
1254        let Ok(Ok(ref installation)) = result else {
1255            return result;
1256        };
1257
1258        // Check if we need to skip the interpreter because it is "not allowed", e.g., if it is a
1259        // pre-release version or an alternative implementation, using it requires opt-in.
1260
1261        // If the interpreter has a default executable name, e.g. `python`, and was found on the
1262        // search path, we consider this opt-in to use it.
1263        let has_default_executable_name = installation.interpreter.has_default_executable_name()
1264            && matches!(
1265                installation.source,
1266                PythonSource::SearchPath | PythonSource::SearchPathFirst
1267            );
1268
1269        // If it's a pre-release and pre-releases aren't allowed, skip it — but store it for later
1270        // since we'll use a pre-release if no other versions are available.
1271        if installation.python_version().pre().is_some()
1272            && !request.allows_prereleases()
1273            && !installation.source.allows_prereleases()
1274            && !has_default_executable_name
1275        {
1276            debug!("Skipping pre-release installation {}", installation.key());
1277            if first_prerelease.is_none() {
1278                first_prerelease = Some(installation.clone());
1279            }
1280            continue;
1281        }
1282
1283        // If it's a debug build and debug builds aren't allowed, skip it — but store it for later
1284        // since we'll use a debug build if no other versions are available.
1285        if installation.key().variant().is_debug()
1286            && !request.allows_debug()
1287            && !installation.source.allows_debug()
1288            && !has_default_executable_name
1289        {
1290            debug!("Skipping debug installation {}", installation.key());
1291            if first_debug.is_none() {
1292                first_debug = Some(installation.clone());
1293            }
1294            continue;
1295        }
1296
1297        // If it's an alternative implementation and alternative implementations aren't allowed,
1298        // skip it. Note we avoid querying these interpreters at all if they're on the search path
1299        // and are not requested, but other sources such as the managed installations can include
1300        // them.
1301        if installation.is_alternative_implementation()
1302            && !request.allows_alternative_implementations()
1303            && !installation.source.allows_alternative_implementations()
1304            && !has_default_executable_name
1305        {
1306            debug!("Skipping alternative implementation {}", installation.key());
1307            continue;
1308        }
1309
1310        // If it's a managed Python installation, and system interpreters are preferred, skip it
1311        // for now.
1312        if matches!(preference, PythonPreference::System) && installation.is_managed() {
1313            debug!(
1314                "Skipping managed installation {}: system installation preferred",
1315                installation.key()
1316            );
1317            if first_managed.is_none() {
1318                first_managed = Some(installation.clone());
1319            }
1320            continue;
1321        }
1322
1323        // If we didn't skip it, this is the installation to use
1324        return result;
1325    }
1326
1327    // If we only found managed installations, and the preference allows them, we should return
1328    // the first one.
1329    if let Some(installation) = first_managed {
1330        debug!(
1331            "Allowing managed installation {}: no system installations",
1332            installation.key()
1333        );
1334        return Ok(Ok(installation));
1335    }
1336
1337    // If we only found debug installations, they're implicitly allowed and we should return the
1338    // first one.
1339    if let Some(installation) = first_debug {
1340        debug!(
1341            "Allowing debug installation {}: no non-debug installations",
1342            installation.key()
1343        );
1344        return Ok(Ok(installation));
1345    }
1346
1347    // If we only found pre-releases, they're implicitly allowed and we should return the first one.
1348    if let Some(installation) = first_prerelease {
1349        debug!(
1350            "Allowing pre-release installation {}: no stable installations",
1351            installation.key()
1352        );
1353        return Ok(Ok(installation));
1354    }
1355
1356    // If we found a Python, but it was unusable for some reason, report that instead of saying we
1357    // couldn't find any Python interpreters.
1358    if let Some(err) = first_error {
1359        return Err(err);
1360    }
1361
1362    Ok(Err(PythonNotFound {
1363        request: request.clone(),
1364        environment_preference: environments,
1365        python_preference: preference,
1366    }))
1367}
1368
1369/// Find the best-matching Python installation.
1370///
1371/// If no Python version is provided, we will use the first available installation.
1372///
1373/// If a Python version is provided, we will first try to find an exact match. If
1374/// that cannot be found and a patch version was requested, we will look for a match
1375/// without comparing the patch version number. If that cannot be found, we fall back to
1376/// the first available version.
1377///
1378/// At all points, if the specified version cannot be found, we will attempt to
1379/// download it if downloads are enabled.
1380///
1381/// See [`find_python_installation`] for more details on installation discovery.
1382#[instrument(skip_all, fields(request))]
1383pub(crate) async fn find_best_python_installation(
1384    request: &PythonRequest,
1385    environments: EnvironmentPreference,
1386    preference: PythonPreference,
1387    downloads_enabled: bool,
1388    download_list: &ManagedPythonDownloadList,
1389    client: &BaseClient,
1390    retry_policy: &ExponentialBackoff,
1391    cache: &Cache,
1392    reporter: Option<&dyn crate::downloads::Reporter>,
1393    python_install_mirror: Option<&str>,
1394    pypy_install_mirror: Option<&str>,
1395    preview: Preview,
1396) -> Result<PythonInstallation, crate::Error> {
1397    debug!("Starting Python discovery for {request}");
1398    let original_request = request;
1399
1400    let mut previous_fetch_failed = false;
1401
1402    let request_without_patch = match request {
1403        PythonRequest::Version(version) => {
1404            if version.has_patch() {
1405                Some(PythonRequest::Version(version.clone().without_patch()))
1406            } else {
1407                None
1408            }
1409        }
1410        PythonRequest::ImplementationVersion(implementation, version) => Some(
1411            PythonRequest::ImplementationVersion(*implementation, version.clone().without_patch()),
1412        ),
1413        _ => None,
1414    };
1415
1416    for (attempt, request) in iter::once(original_request)
1417        .chain(request_without_patch.iter())
1418        .chain(iter::once(&PythonRequest::Default))
1419        .enumerate()
1420    {
1421        debug!(
1422            "Looking for {request}{}",
1423            if request != original_request {
1424                format!(" attempt {attempt} (fallback after failing to find: {original_request})")
1425            } else {
1426                String::new()
1427            }
1428        );
1429        let result = find_python_installation(request, environments, preference, cache, preview);
1430        let error = match result {
1431            Ok(Ok(installation)) => {
1432                warn_on_unsupported_python(installation.interpreter());
1433                return Ok(installation);
1434            }
1435            // Continue if we can't find a matching Python and ignore non-critical discovery errors
1436            Ok(Err(error)) => error.into(),
1437            Err(error) if !error.is_critical() => error.into(),
1438            Err(error) => return Err(error.into()),
1439        };
1440
1441        // Attempt to download the version if downloads are enabled
1442        if downloads_enabled
1443            && !previous_fetch_failed
1444            && let Some(download_request) = PythonDownloadRequest::from_request(request)
1445        {
1446            let download = download_request
1447                .clone()
1448                .fill()
1449                .map(|request| download_list.find(&request));
1450
1451            let result = match download {
1452                Ok(Ok(download)) => PythonInstallation::fetch(
1453                    download,
1454                    client,
1455                    retry_policy,
1456                    cache,
1457                    reporter,
1458                    python_install_mirror,
1459                    pypy_install_mirror,
1460                )
1461                .await
1462                .map(Some),
1463                Ok(Err(crate::downloads::Error::NoDownloadFound(_))) => Ok(None),
1464                Ok(Err(error)) => Err(error.into()),
1465                Err(error) => Err(error.into()),
1466            };
1467            if let Ok(Some(installation)) = result {
1468                return Ok(installation);
1469            }
1470            // Emit a warning instead of failing since we may find a suitable
1471            // interpreter on the system after relaxing the request further.
1472            // Additionally, uv did not previously attempt downloads in this
1473            // code path and we want to minimize the fatal cases for
1474            // backwards compatibility.
1475            // Errors encountered here are either network errors or quirky
1476            // configuration problems.
1477            if let Err(error) = result {
1478                // If the request was for the default or any version, propagate
1479                // the error as nothing else we are about to do will help the
1480                // situation.
1481                if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1482                    return Err(error);
1483                }
1484
1485                let mut error_chain = String::new();
1486                // Writing to a string can't fail with errors (panics on allocation failure)
1487                let error = anyhow::Error::from(error).context(format!(
1488                    "A managed Python download is available for {request}, but an error occurred when attempting to download it."
1489                ));
1490                uv_warnings::write_error_chain(
1491                    error.as_ref(),
1492                    &mut error_chain,
1493                    "warning",
1494                    AnsiColors::Yellow,
1495                )
1496                .unwrap();
1497                anstream::eprint!("{}", error_chain);
1498                previous_fetch_failed = true;
1499            }
1500        }
1501
1502        // If this was a request for the Default or Any version, this means that
1503        // either that's what we were called with, or we're on the last
1504        // iteration.
1505        //
1506        // The most recent find error therefore becomes a fatal one.
1507        if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1508            return Err(match error {
1509                crate::Error::MissingPython(err, _) => PythonNotFound {
1510                    // Use a more general error in this case since we looked for multiple versions
1511                    request: original_request.clone(),
1512                    python_preference: err.python_preference,
1513                    environment_preference: err.environment_preference,
1514                }
1515                .into(),
1516                other => other,
1517            });
1518        }
1519    }
1520
1521    unreachable!("The loop should have terminated when it reached PythonRequest::Default");
1522}
1523
1524/// Display a warning if the Python version of the [`Interpreter`] is unsupported by uv.
1525fn warn_on_unsupported_python(interpreter: &Interpreter) {
1526    // Warn on usage with an unsupported Python version
1527    if interpreter.python_tuple() < (3, 8) {
1528        warn_user_once!(
1529            "uv is only compatible with Python >=3.8, found Python {}",
1530            interpreter.python_version()
1531        );
1532    }
1533}
1534
1535/// On Windows we might encounter the Windows Store proxy shim (enabled in:
1536/// Settings/Apps/Advanced app settings/App execution aliases). When Python is _not_ installed
1537/// via the Windows Store, but the proxy shim is enabled, then executing `python.exe` or
1538/// `python3.exe` will redirect to the Windows Store installer.
1539///
1540/// We need to detect that these `python.exe` and `python3.exe` files are _not_ Python
1541/// executables.
1542///
1543/// This method is taken from Rye:
1544///
1545/// > This is a pretty dumb way.  We know how to parse this reparse point, but Microsoft
1546/// > does not want us to do this as the format is unstable.  So this is a best effort way.
1547/// > we just hope that the reparse point has the python redirector in it, when it's not
1548/// > pointing to a valid Python.
1549///
1550/// See: <https://github.com/astral-sh/rye/blob/b0e9eccf05fe4ff0ae7b0250a248c54f2d780b4d/rye/src/cli/shim.rs#L108>
1551#[cfg(windows)]
1552pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
1553    use std::os::windows::fs::MetadataExt;
1554    use std::os::windows::prelude::OsStrExt;
1555    use windows::Win32::Foundation::CloseHandle;
1556    use windows::Win32::Storage::FileSystem::{
1557        CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
1558        FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_MODE, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
1559        OPEN_EXISTING,
1560    };
1561    use windows::Win32::System::IO::DeviceIoControl;
1562    use windows::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
1563    use windows::core::PCWSTR;
1564
1565    // The path must be absolute.
1566    if !path.is_absolute() {
1567        return false;
1568    }
1569
1570    // The path must point to something like:
1571    //   `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python3.exe`
1572    let mut components = path.components().rev();
1573
1574    // Ex) `python.exe`, `python3.exe`, `python3.12.exe`, etc.
1575    if !components
1576        .next()
1577        .and_then(|component| component.as_os_str().to_str())
1578        .is_some_and(|component| {
1579            component.starts_with("python")
1580                && std::path::Path::new(component)
1581                    .extension()
1582                    .is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
1583        })
1584    {
1585        return false;
1586    }
1587
1588    // Ex) `WindowsApps`
1589    if components
1590        .next()
1591        .is_none_or(|component| component.as_os_str() != "WindowsApps")
1592    {
1593        return false;
1594    }
1595
1596    // Ex) `Microsoft`
1597    if components
1598        .next()
1599        .is_none_or(|component| component.as_os_str() != "Microsoft")
1600    {
1601        return false;
1602    }
1603
1604    // The file is only relevant if it's a reparse point.
1605    let Ok(md) = fs_err::symlink_metadata(path) else {
1606        return false;
1607    };
1608    if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 {
1609        return false;
1610    }
1611
1612    let mut path_encoded = path
1613        .as_os_str()
1614        .encode_wide()
1615        .chain(std::iter::once(0))
1616        .collect::<Vec<_>>();
1617
1618    // SAFETY: The path is null-terminated.
1619    #[allow(unsafe_code)]
1620    let reparse_handle = unsafe {
1621        CreateFileW(
1622            PCWSTR(path_encoded.as_mut_ptr()),
1623            0,
1624            FILE_SHARE_MODE(0),
1625            None,
1626            OPEN_EXISTING,
1627            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
1628            None,
1629        )
1630    };
1631
1632    let Ok(reparse_handle) = reparse_handle else {
1633        return false;
1634    };
1635
1636    let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
1637    let mut bytes_returned = 0;
1638
1639    // SAFETY: The buffer is large enough to hold the reparse point.
1640    #[allow(unsafe_code, clippy::cast_possible_truncation)]
1641    let success = unsafe {
1642        DeviceIoControl(
1643            reparse_handle,
1644            FSCTL_GET_REPARSE_POINT,
1645            None,
1646            0,
1647            Some(buf.as_mut_ptr().cast()),
1648            buf.len() as u32 * 2,
1649            Some(&raw mut bytes_returned),
1650            None,
1651        )
1652        .is_ok()
1653    };
1654
1655    // SAFETY: The handle is valid.
1656    #[allow(unsafe_code)]
1657    unsafe {
1658        let _ = CloseHandle(reparse_handle);
1659    }
1660
1661    // If the operation failed, assume it's not a reparse point.
1662    if !success {
1663        return false;
1664    }
1665
1666    let reparse_point = String::from_utf16_lossy(&buf[..bytes_returned as usize]);
1667    reparse_point.contains("\\AppInstallerPythonRedirector.exe")
1668}
1669
1670/// On Unix, we do not need to deal with Windows store shims.
1671///
1672/// See the Windows implementation for details.
1673#[cfg(not(windows))]
1674fn is_windows_store_shim(_path: &Path) -> bool {
1675    false
1676}
1677
1678impl PythonVariant {
1679    fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
1680        match self {
1681            Self::Default => {
1682                // TODO(zanieb): Right now, we allow debug interpreters to be selected by default for
1683                // backwards compatibility, but we may want to change this in the future.
1684                if (interpreter.python_major(), interpreter.python_minor()) >= (3, 14) {
1685                    // For Python 3.14+, the free-threaded build is not considered experimental
1686                    // and can satisfy the default variant without opt-in
1687                    true
1688                } else {
1689                    // In Python 3.13 and earlier, the free-threaded build is considered
1690                    // experimental and requires explicit opt-in
1691                    !interpreter.gil_disabled()
1692                }
1693            }
1694            Self::Debug => interpreter.debug_enabled(),
1695            Self::Freethreaded => interpreter.gil_disabled(),
1696            Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
1697            Self::Gil => !interpreter.gil_disabled(),
1698            Self::GilDebug => !interpreter.gil_disabled() && interpreter.debug_enabled(),
1699        }
1700    }
1701
1702    /// Return the executable suffix for the variant, e.g., `t` for `python3.13t`.
1703    ///
1704    /// Returns an empty string for the default Python variant.
1705    pub fn executable_suffix(self) -> &'static str {
1706        match self {
1707            Self::Default => "",
1708            Self::Debug => "d",
1709            Self::Freethreaded => "t",
1710            Self::FreethreadedDebug => "td",
1711            Self::Gil => "",
1712            Self::GilDebug => "d",
1713        }
1714    }
1715
1716    /// Return the suffix for display purposes, e.g., `+gil`.
1717    pub fn display_suffix(self) -> &'static str {
1718        match self {
1719            Self::Default => "",
1720            Self::Debug => "+debug",
1721            Self::Freethreaded => "+freethreaded",
1722            Self::FreethreadedDebug => "+freethreaded+debug",
1723            Self::Gil => "+gil",
1724            Self::GilDebug => "+gil+debug",
1725        }
1726    }
1727
1728    /// Return the lib suffix for the variant, e.g., `t` for `python3.13t` but an empty string for
1729    /// `python3.13d` or `python3.13`.
1730    pub fn lib_suffix(self) -> &'static str {
1731        match self {
1732            Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "",
1733            Self::Freethreaded | Self::FreethreadedDebug => "t",
1734        }
1735    }
1736
1737    pub fn is_freethreaded(self) -> bool {
1738        match self {
1739            Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false,
1740            Self::Freethreaded | Self::FreethreadedDebug => true,
1741        }
1742    }
1743
1744    pub fn is_debug(self) -> bool {
1745        match self {
1746            Self::Default | Self::Freethreaded | Self::Gil => false,
1747            Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true,
1748        }
1749    }
1750}
1751impl PythonRequest {
1752    /// Create a request from a string.
1753    ///
1754    /// This cannot fail, which means weird inputs will be parsed as [`PythonRequest::File`] or
1755    /// [`PythonRequest::ExecutableName`].
1756    ///
1757    /// This is intended for parsing the argument to the `--python` flag. See also
1758    /// [`try_from_tool_name`][Self::try_from_tool_name] below.
1759    pub fn parse(value: &str) -> Self {
1760        let lowercase_value = &value.to_ascii_lowercase();
1761
1762        // Literals, e.g. `any` or `default`
1763        if lowercase_value == "any" {
1764            return Self::Any;
1765        }
1766        if lowercase_value == "default" {
1767            return Self::Default;
1768        }
1769
1770        // the prefix of e.g. `python312` and the empty prefix of bare versions, e.g. `312`
1771        let abstract_version_prefixes = ["python", ""];
1772        let all_implementation_names =
1773            ImplementationName::long_names().chain(ImplementationName::short_names());
1774        // Abstract versions like `python@312`, `python312`, or `312`, plus implementations and
1775        // implementation versions like `pypy`, `pypy@312` or `pypy312`.
1776        if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1777            abstract_version_prefixes,
1778            all_implementation_names,
1779            lowercase_value,
1780        ) {
1781            return request;
1782        }
1783
1784        let value_as_path = PathBuf::from(value);
1785        // e.g. /path/to/.venv
1786        if value_as_path.is_dir() {
1787            return Self::Directory(value_as_path);
1788        }
1789        // e.g. /path/to/python
1790        if value_as_path.is_file() {
1791            return Self::File(value_as_path);
1792        }
1793
1794        // e.g. path/to/python on Windows, where path/to/python.exe is the true path
1795        #[cfg(windows)]
1796        if value_as_path.extension().is_none() {
1797            let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1798            if value_as_path.is_file() {
1799                return Self::File(value_as_path);
1800            }
1801        }
1802
1803        // During unit testing, we cannot change the working directory used by std
1804        // so we perform a check relative to the mock working directory. Ideally we'd
1805        // remove this code and use tests at the CLI level so we can change the real
1806        // directory.
1807        #[cfg(test)]
1808        if value_as_path.is_relative() {
1809            if let Ok(current_dir) = crate::current_dir() {
1810                let relative = current_dir.join(&value_as_path);
1811                if relative.is_dir() {
1812                    return Self::Directory(relative);
1813                }
1814                if relative.is_file() {
1815                    return Self::File(relative);
1816                }
1817            }
1818        }
1819        // e.g. .\path\to\python3.exe or ./path/to/python3
1820        // If it contains a path separator, we'll treat it as a full path even if it does not exist
1821        if value.contains(std::path::MAIN_SEPARATOR) {
1822            return Self::File(value_as_path);
1823        }
1824        // e.g. ./path/to/python3.exe
1825        // On Windows, Unix path separators are often valid
1826        if cfg!(windows) && value.contains('/') {
1827            return Self::File(value_as_path);
1828        }
1829        if let Ok(request) = PythonDownloadRequest::from_str(value) {
1830            return Self::Key(request);
1831        }
1832        // Finally, we'll treat it as the name of an executable (i.e. in the search PATH)
1833        // e.g. foo.exe
1834        Self::ExecutableName(value.to_string())
1835    }
1836
1837    /// Try to parse a tool name as a Python version, e.g. `uvx python311`.
1838    ///
1839    /// The `PythonRequest::parse` constructor above is intended for the `--python` flag, where the
1840    /// value is unambiguously a Python version. This alternate constructor is intended for `uvx`
1841    /// or `uvx --from`, where the executable could be either a Python version or a package name.
1842    /// There are several differences in behavior:
1843    ///
1844    /// - This only supports long names, including e.g. `pypy39` but **not** `pp39` or `39`.
1845    /// - On Windows only, this allows `pythonw` as an alias for `python`.
1846    /// - This allows `python` by itself (and on Windows, `pythonw`) as an alias for `default`.
1847    ///
1848    /// This can only return `Err` if `@` is used. Otherwise, if no match is found, it returns
1849    /// `Ok(None)`.
1850    pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
1851        let lowercase_value = &value.to_ascii_lowercase();
1852        // Omitting the empty string from these lists excludes bare versions like "39".
1853        let abstract_version_prefixes = if cfg!(windows) {
1854            &["python", "pythonw"][..]
1855        } else {
1856            &["python"][..]
1857        };
1858        // e.g. just `python`
1859        if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
1860            return Ok(Some(Self::Default));
1861        }
1862        Self::parse_versions_and_implementations(
1863            abstract_version_prefixes.iter().copied(),
1864            ImplementationName::long_names(),
1865            lowercase_value,
1866        )
1867    }
1868
1869    /// Take a value like `"python3.11"`, check whether it matches a set of abstract python
1870    /// prefixes (e.g. `"python"`, `"pythonw"`, or even `""`) or a set of specific Python
1871    /// implementations (e.g. `"cpython"` or `"pypy"`, possibly with abbreviations), and if so try
1872    /// to parse its version.
1873    ///
1874    /// This can only return `Err` if `@` is used, see
1875    /// [`try_split_prefix_and_version`][Self::try_split_prefix_and_version] below. Otherwise, if
1876    /// no match is found, it returns `Ok(None)`.
1877    fn parse_versions_and_implementations<'a>(
1878        // typically "python", possibly also "pythonw" or "" (for bare versions)
1879        abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
1880        // expected to be either long_names() or all names
1881        implementation_names: impl IntoIterator<Item = &'a str>,
1882        // the string to parse
1883        lowercase_value: &str,
1884    ) -> Result<Option<Self>, Error> {
1885        for prefix in abstract_version_prefixes {
1886            if let Some(version_request) =
1887                Self::try_split_prefix_and_version(prefix, lowercase_value)?
1888            {
1889                // e.g. `python39` or `python@39`
1890                // Note that e.g. `python` gets handled elsewhere, if at all. (It's currently
1891                // allowed in tool executables but not in --python flags.)
1892                return Ok(Some(Self::Version(version_request)));
1893            }
1894        }
1895        for implementation in implementation_names {
1896            if lowercase_value == implementation {
1897                return Ok(Some(Self::Implementation(
1898                    // e.g. `pypy`
1899                    // Safety: The name matched the possible names above
1900                    ImplementationName::from_str(implementation).unwrap(),
1901                )));
1902            }
1903            if let Some(version_request) =
1904                Self::try_split_prefix_and_version(implementation, lowercase_value)?
1905            {
1906                // e.g. `pypy39`
1907                return Ok(Some(Self::ImplementationVersion(
1908                    // Safety: The name matched the possible names above
1909                    ImplementationName::from_str(implementation).unwrap(),
1910                    version_request,
1911                )));
1912            }
1913        }
1914        Ok(None)
1915    }
1916
1917    /// Take a value like `"python3.11"`, check whether it matches a target prefix (e.g.
1918    /// `"python"`, `"pypy"`, or even `""`), and if so try to parse its version.
1919    ///
1920    /// Failing to match the prefix (e.g. `"notpython3.11"`) or failing to parse a version (e.g.
1921    /// `"python3notaversion"`) is not an error, and those cases return `Ok(None)`. The `@`
1922    /// separator is optional, and this function can only return `Err` if `@` is used. There are
1923    /// two error cases:
1924    ///
1925    /// - The value starts with `@` (e.g. `@3.11`).
1926    /// - The prefix is a match, but the version is invalid (e.g. `python@3.not.a.version`).
1927    fn try_split_prefix_and_version(
1928        prefix: &str,
1929        lowercase_value: &str,
1930    ) -> Result<Option<VersionRequest>, Error> {
1931        if lowercase_value.starts_with('@') {
1932            return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
1933        }
1934        let Some(rest) = lowercase_value.strip_prefix(prefix) else {
1935            return Ok(None);
1936        };
1937        // Just the prefix by itself (e.g. "python") is handled elsewhere.
1938        if rest.is_empty() {
1939            return Ok(None);
1940        }
1941        // The @ separator is optional. If it's present, the right half must be a version, and
1942        // parsing errors are raised to the caller.
1943        if let Some(after_at) = rest.strip_prefix('@') {
1944            if after_at == "latest" {
1945                // Handle `@latest` as a special case. It's still an error for now, but we plan to
1946                // support it. TODO(zanieb): Add `PythonRequest::Latest`
1947                return Err(Error::LatestVersionRequest);
1948            }
1949            return after_at.parse().map(Some);
1950        }
1951        // The @ was not present, so if the version fails to parse just return Ok(None). For
1952        // example, python3stuff.
1953        Ok(rest.parse().ok())
1954    }
1955
1956    /// Check if this request includes a specific patch version.
1957    pub fn includes_patch(&self) -> bool {
1958        match self {
1959            Self::Default => false,
1960            Self::Any => false,
1961            Self::Version(version_request) => version_request.patch().is_some(),
1962            Self::Directory(..) => false,
1963            Self::File(..) => false,
1964            Self::ExecutableName(..) => false,
1965            Self::Implementation(..) => false,
1966            Self::ImplementationVersion(_, version) => version.patch().is_some(),
1967            Self::Key(request) => request
1968                .version
1969                .as_ref()
1970                .is_some_and(|request| request.patch().is_some()),
1971        }
1972    }
1973
1974    /// Check if this request includes a specific prerelease version.
1975    pub fn includes_prerelease(&self) -> bool {
1976        match self {
1977            Self::Default => false,
1978            Self::Any => false,
1979            Self::Version(version_request) => version_request.prerelease().is_some(),
1980            Self::Directory(..) => false,
1981            Self::File(..) => false,
1982            Self::ExecutableName(..) => false,
1983            Self::Implementation(..) => false,
1984            Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
1985            Self::Key(request) => request
1986                .version
1987                .as_ref()
1988                .is_some_and(|request| request.prerelease().is_some()),
1989        }
1990    }
1991
1992    /// Check if a given interpreter satisfies the interpreter request.
1993    pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
1994        /// Returns `true` if the two paths refer to the same interpreter executable.
1995        fn is_same_executable(path1: &Path, path2: &Path) -> bool {
1996            path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
1997        }
1998
1999        match self {
2000            Self::Default | Self::Any => true,
2001            Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2002            Self::Directory(directory) => {
2003                // `sys.prefix` points to the environment root or `sys.executable` is the same
2004                is_same_executable(directory, interpreter.sys_prefix())
2005                    || is_same_executable(
2006                        virtualenv_python_executable(directory).as_path(),
2007                        interpreter.sys_executable(),
2008                    )
2009            }
2010            Self::File(file) => {
2011                // The interpreter satisfies the request both if it is the venv...
2012                if is_same_executable(interpreter.sys_executable(), file) {
2013                    return true;
2014                }
2015                // ...or if it is the base interpreter the venv was created from.
2016                if interpreter
2017                    .sys_base_executable()
2018                    .is_some_and(|sys_base_executable| {
2019                        is_same_executable(sys_base_executable, file)
2020                    })
2021                {
2022                    return true;
2023                }
2024                // ...or, on Windows, if both interpreters have the same base executable. On
2025                // Windows, interpreters are copied rather than symlinked, so a virtual environment
2026                // created from within a virtual environment will _not_ evaluate to the same
2027                // `sys.executable`, but will have the same `sys._base_executable`.
2028                if cfg!(windows) {
2029                    if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2030                        if let (Some(file_base), Some(interpreter_base)) = (
2031                            file_interpreter.sys_base_executable(),
2032                            interpreter.sys_base_executable(),
2033                        ) {
2034                            if is_same_executable(file_base, interpreter_base) {
2035                                return true;
2036                            }
2037                        }
2038                    }
2039                }
2040                false
2041            }
2042            Self::ExecutableName(name) => {
2043                // First, see if we have a match in the venv ...
2044                if interpreter
2045                    .sys_executable()
2046                    .file_name()
2047                    .is_some_and(|filename| filename == name.as_str())
2048                {
2049                    return true;
2050                }
2051                // ... or the venv's base interpreter (without performing IO), if that fails, ...
2052                if interpreter
2053                    .sys_base_executable()
2054                    .and_then(|executable| executable.file_name())
2055                    .is_some_and(|file_name| file_name == name.as_str())
2056                {
2057                    return true;
2058                }
2059                // ... check in `PATH`. The name we find here does not need to be the
2060                // name we install, so we can find `foopython` here which got installed as `python`.
2061                if which(name)
2062                    .ok()
2063                    .as_ref()
2064                    .and_then(|executable| executable.file_name())
2065                    .is_some_and(|file_name| file_name == name.as_str())
2066                {
2067                    return true;
2068                }
2069                false
2070            }
2071            Self::Implementation(implementation) => interpreter
2072                .implementation_name()
2073                .eq_ignore_ascii_case(implementation.into()),
2074            Self::ImplementationVersion(implementation, version) => {
2075                version.matches_interpreter(interpreter)
2076                    && interpreter
2077                        .implementation_name()
2078                        .eq_ignore_ascii_case(implementation.into())
2079            }
2080            Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2081        }
2082    }
2083
2084    /// Whether this request opts-in to a pre-release Python version.
2085    pub(crate) fn allows_prereleases(&self) -> bool {
2086        match self {
2087            Self::Default => false,
2088            Self::Any => true,
2089            Self::Version(version) => version.allows_prereleases(),
2090            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2091            Self::Implementation(_) => false,
2092            Self::ImplementationVersion(_, _) => true,
2093            Self::Key(request) => request.allows_prereleases(),
2094        }
2095    }
2096
2097    /// Whether this request opts-in to a debug Python version.
2098    pub(crate) fn allows_debug(&self) -> bool {
2099        match self {
2100            Self::Default => false,
2101            Self::Any => true,
2102            Self::Version(version) => version.is_debug(),
2103            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2104            Self::Implementation(_) => false,
2105            Self::ImplementationVersion(_, _) => true,
2106            Self::Key(request) => request.allows_debug(),
2107        }
2108    }
2109
2110    /// Whether this request opts-in to an alternative Python implementation, e.g., PyPy.
2111    pub(crate) fn allows_alternative_implementations(&self) -> bool {
2112        match self {
2113            Self::Default => false,
2114            Self::Any => true,
2115            Self::Version(_) => false,
2116            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2117            Self::Implementation(implementation)
2118            | Self::ImplementationVersion(implementation, _) => {
2119                !matches!(implementation, ImplementationName::CPython)
2120            }
2121            Self::Key(request) => request.allows_alternative_implementations(),
2122        }
2123    }
2124
2125    pub(crate) fn is_explicit_system(&self) -> bool {
2126        matches!(self, Self::File(_) | Self::Directory(_))
2127    }
2128
2129    /// Serialize the request to a canonical representation.
2130    ///
2131    /// [`Self::parse`] should always return the same request when given the output of this method.
2132    pub fn to_canonical_string(&self) -> String {
2133        match self {
2134            Self::Any => "any".to_string(),
2135            Self::Default => "default".to_string(),
2136            Self::Version(version) => version.to_string(),
2137            Self::Directory(path) => path.display().to_string(),
2138            Self::File(path) => path.display().to_string(),
2139            Self::ExecutableName(name) => name.clone(),
2140            Self::Implementation(implementation) => implementation.to_string(),
2141            Self::ImplementationVersion(implementation, version) => {
2142                format!("{implementation}@{version}")
2143            }
2144            Self::Key(request) => request.to_string(),
2145        }
2146    }
2147
2148    /// Convert an interpreter request into a concrete PEP 440 `Version` when possible.
2149    ///
2150    /// Returns `None` if the request doesn't carry an exact version
2151    pub fn as_pep440_version(&self) -> Option<Version> {
2152        match self {
2153            Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2154            Self::Key(download_request) => download_request
2155                .version()
2156                .and_then(VersionRequest::as_pep440_version),
2157            Self::Default
2158            | Self::Any
2159            | Self::Directory(_)
2160            | Self::File(_)
2161            | Self::ExecutableName(_)
2162            | Self::Implementation(_) => None,
2163        }
2164    }
2165
2166    /// Convert an interpreter request into [`VersionSpecifiers`] representing the range of
2167    /// compatible versions.
2168    ///
2169    /// Returns `None` if the request doesn't carry version constraints (e.g., a path or
2170    /// executable name).
2171    pub fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2172        match self {
2173            Self::Version(version) | Self::ImplementationVersion(_, version) => {
2174                version.as_version_specifiers()
2175            }
2176            Self::Key(download_request) => download_request
2177                .version()
2178                .and_then(VersionRequest::as_version_specifiers),
2179            Self::Default
2180            | Self::Any
2181            | Self::Directory(_)
2182            | Self::File(_)
2183            | Self::ExecutableName(_)
2184            | Self::Implementation(_) => None,
2185        }
2186    }
2187
2188    /// Returns `true` when this request is compatible with the given `requires-python` specifier.
2189    ///
2190    /// Requests without version constraints (e.g., paths, executable names) are always considered
2191    /// compatible. For versioned requests, compatibility means the request's version range has a
2192    /// non-empty intersection with the `requires-python` range.
2193    pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2194        let Some(specifiers) = self.as_version_specifiers() else {
2195            return true;
2196        };
2197
2198        let request_range = release_specifiers_to_ranges(specifiers);
2199        let requires_python_range =
2200            release_specifiers_to_ranges(requires_python.specifiers().clone());
2201        !request_range
2202            .intersection(&requires_python_range)
2203            .is_empty()
2204    }
2205}
2206
2207impl PythonSource {
2208    pub fn is_managed(self) -> bool {
2209        matches!(self, Self::Managed)
2210    }
2211
2212    /// Whether a pre-release Python installation from this source can be used without opt-in.
2213    pub(crate) fn allows_prereleases(self) -> bool {
2214        match self {
2215            Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2216            Self::SearchPath
2217            | Self::SearchPathFirst
2218            | Self::CondaPrefix
2219            | Self::BaseCondaPrefix
2220            | Self::ProvidedPath
2221            | Self::ParentInterpreter
2222            | Self::ActiveEnvironment
2223            | Self::DiscoveredEnvironment => true,
2224        }
2225    }
2226
2227    /// Whether a debug Python installation from this source can be used without opt-in.
2228    pub(crate) fn allows_debug(self) -> bool {
2229        match self {
2230            Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2231            Self::SearchPath
2232            | Self::SearchPathFirst
2233            | Self::CondaPrefix
2234            | Self::BaseCondaPrefix
2235            | Self::ProvidedPath
2236            | Self::ParentInterpreter
2237            | Self::ActiveEnvironment
2238            | Self::DiscoveredEnvironment => true,
2239        }
2240    }
2241
2242    /// Whether an alternative Python implementation from this source can be used without opt-in.
2243    pub(crate) fn allows_alternative_implementations(self) -> bool {
2244        match self {
2245            Self::Managed
2246            | Self::Registry
2247            | Self::SearchPath
2248            // TODO(zanieb): We may want to allow this at some point, but when adding this variant
2249            // we want compatibility with existing behavior
2250            | Self::SearchPathFirst
2251            | Self::MicrosoftStore => false,
2252            Self::CondaPrefix
2253            | Self::BaseCondaPrefix
2254            | Self::ProvidedPath
2255            | Self::ParentInterpreter
2256            | Self::ActiveEnvironment
2257            | Self::DiscoveredEnvironment => true,
2258        }
2259    }
2260
2261    /// Whether this source **could** be a virtual environment.
2262    ///
2263    /// This excludes the [`PythonSource::SearchPath`] although it could be in a virtual
2264    /// environment; pragmatically, that's not common and saves us from querying a bunch of system
2265    /// interpreters for no reason. It seems dubious to consider an interpreter in the `PATH` as a
2266    /// target virtual environment if it's not discovered through our virtual environment-specific
2267    /// patterns. Instead, we special case the first Python executable found on the `PATH` with
2268    /// [`PythonSource::SearchPathFirst`], allowing us to check if that's a virtual environment.
2269    /// This enables targeting the virtual environment with uv by putting its `bin/` on the `PATH`
2270    /// without setting `VIRTUAL_ENV` — but if there's another interpreter before it we will ignore
2271    /// it.
2272    pub(crate) fn is_maybe_virtualenv(self) -> bool {
2273        match self {
2274            Self::ProvidedPath
2275            | Self::ActiveEnvironment
2276            | Self::DiscoveredEnvironment
2277            | Self::CondaPrefix
2278            | Self::BaseCondaPrefix
2279            | Self::ParentInterpreter
2280            | Self::SearchPathFirst => true,
2281            Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2282        }
2283    }
2284
2285    /// Whether this source is "explicit", e.g., it was directly provided by the user or is
2286    /// an active virtual environment.
2287    pub(crate) fn is_explicit(self) -> bool {
2288        match self {
2289            Self::ProvidedPath
2290            | Self::ParentInterpreter
2291            | Self::ActiveEnvironment
2292            | Self::CondaPrefix => true,
2293            Self::Managed
2294            | Self::DiscoveredEnvironment
2295            | Self::SearchPath
2296            | Self::SearchPathFirst
2297            | Self::Registry
2298            | Self::MicrosoftStore
2299            | Self::BaseCondaPrefix => false,
2300        }
2301    }
2302
2303    /// Whether this source **could** be a system interpreter.
2304    pub(crate) fn is_maybe_system(self) -> bool {
2305        match self {
2306            Self::CondaPrefix
2307            | Self::BaseCondaPrefix
2308            | Self::ParentInterpreter
2309            | Self::ProvidedPath
2310            | Self::Managed
2311            | Self::SearchPath
2312            | Self::SearchPathFirst
2313            | Self::Registry
2314            | Self::MicrosoftStore => true,
2315            Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2316        }
2317    }
2318}
2319
2320impl PythonPreference {
2321    fn allows_source(self, source: PythonSource) -> bool {
2322        // If not dealing with a system interpreter source, we don't care about the preference
2323        if !matches!(
2324            source,
2325            PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2326        ) {
2327            return true;
2328        }
2329
2330        match self {
2331            Self::OnlyManaged => matches!(source, PythonSource::Managed),
2332            Self::Managed | Self::System => matches!(
2333                source,
2334                PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2335            ),
2336            Self::OnlySystem => {
2337                matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2338            }
2339        }
2340    }
2341
2342    pub(crate) fn allows_managed(self) -> bool {
2343        match self {
2344            Self::OnlySystem => false,
2345            Self::Managed | Self::System | Self::OnlyManaged => true,
2346        }
2347    }
2348
2349    /// Returns `true` if the given interpreter is allowed by this preference.
2350    ///
2351    /// Unlike [`PythonPreference::allows_source`], which checks the [`PythonSource`], this checks
2352    /// whether the interpreter's base prefix is in a managed location.
2353    pub fn allows_interpreter(self, interpreter: &Interpreter) -> bool {
2354        match self {
2355            Self::OnlyManaged => interpreter.is_managed(),
2356            Self::OnlySystem => !interpreter.is_managed(),
2357            Self::Managed | Self::System => true,
2358        }
2359    }
2360
2361    /// Returns `true` if the given installation is allowed by this preference.
2362    ///
2363    /// Explicit sources (e.g., provided paths, active environments) are always allowed, even if
2364    /// they conflict with the preference. We may want to invalidate the environment in some
2365    /// cases, like in projects, but we can't distinguish between explicit requests for a
2366    /// different Python preference or a persistent preference in a configuration file which
2367    /// would result in overly aggressive invalidation.
2368    pub fn allows_installation(self, installation: &PythonInstallation) -> bool {
2369        let source = installation.source;
2370        let interpreter = &installation.interpreter;
2371
2372        match self {
2373            Self::OnlyManaged => {
2374                if self.allows_interpreter(interpreter) {
2375                    true
2376                } else if source.is_explicit() {
2377                    debug!(
2378                        "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2379                        interpreter.sys_executable().display()
2380                    );
2381                    true
2382                } else {
2383                    debug!(
2384                        "Ignoring Python interpreter at `{}`: only managed interpreters allowed",
2385                        interpreter.sys_executable().display()
2386                    );
2387                    false
2388                }
2389            }
2390            // If not "only" a kind, any interpreter is okay
2391            Self::Managed | Self::System => true,
2392            Self::OnlySystem => {
2393                if self.allows_interpreter(interpreter) {
2394                    true
2395                } else if source.is_explicit() {
2396                    debug!(
2397                        "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2398                        interpreter.sys_executable().display()
2399                    );
2400                    true
2401                } else {
2402                    debug!(
2403                        "Ignoring Python interpreter at `{}`: only system interpreters allowed",
2404                        interpreter.sys_executable().display()
2405                    );
2406                    false
2407                }
2408            }
2409        }
2410    }
2411
2412    /// Returns a new preference when the `--system` flag is used.
2413    ///
2414    /// This will convert [`PythonPreference::Managed`] to [`PythonPreference::System`] when system
2415    /// is set.
2416    #[must_use]
2417    pub fn with_system_flag(self, system: bool) -> Self {
2418        match self {
2419            // TODO(zanieb): It's not clear if we want to allow `--system` to override
2420            // `--managed-python`. We should probably make this `from_system_flag` and refactor
2421            // handling of the `PythonPreference` to use an `Option` so we can tell if the user
2422            // provided it?
2423            Self::OnlyManaged => self,
2424            Self::Managed => {
2425                if system {
2426                    Self::System
2427                } else {
2428                    self
2429                }
2430            }
2431            Self::System => self,
2432            Self::OnlySystem => self,
2433        }
2434    }
2435}
2436
2437impl PythonDownloads {
2438    pub fn is_automatic(self) -> bool {
2439        matches!(self, Self::Automatic)
2440    }
2441}
2442
2443impl EnvironmentPreference {
2444    pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2445        match (system, mutable) {
2446            // When the system flag is provided, ignore virtual environments.
2447            (true, _) => Self::OnlySystem,
2448            // For mutable operations, only allow discovery of the system with explicit selection.
2449            (false, true) => Self::ExplicitSystem,
2450            // For immutable operations, we allow discovery of the system environment
2451            (false, false) => Self::Any,
2452        }
2453    }
2454}
2455
2456#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2457pub(crate) struct ExecutableName {
2458    implementation: Option<ImplementationName>,
2459    major: Option<u8>,
2460    minor: Option<u8>,
2461    patch: Option<u8>,
2462    prerelease: Option<Prerelease>,
2463    variant: PythonVariant,
2464}
2465
2466#[derive(Debug, Clone, PartialEq, Eq)]
2467struct ExecutableNameComparator<'a> {
2468    name: ExecutableName,
2469    request: &'a VersionRequest,
2470    implementation: Option<&'a ImplementationName>,
2471}
2472
2473impl Ord for ExecutableNameComparator<'_> {
2474    /// Note the comparison returns a reverse priority ordering.
2475    ///
2476    /// Higher priority items are "Greater" than lower priority items.
2477    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2478        // Prefer the default name over a specific implementation, unless an implementation was
2479        // requested
2480        let name_ordering = if self.implementation.is_some() {
2481            std::cmp::Ordering::Greater
2482        } else {
2483            std::cmp::Ordering::Less
2484        };
2485        if self.name.implementation.is_none() && other.name.implementation.is_some() {
2486            return name_ordering.reverse();
2487        }
2488        if self.name.implementation.is_some() && other.name.implementation.is_none() {
2489            return name_ordering;
2490        }
2491        // Otherwise, use the names in supported order
2492        let ordering = self.name.implementation.cmp(&other.name.implementation);
2493        if ordering != std::cmp::Ordering::Equal {
2494            return ordering;
2495        }
2496        let ordering = self.name.major.cmp(&other.name.major);
2497        let is_default_request =
2498            matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2499        if ordering != std::cmp::Ordering::Equal {
2500            return if is_default_request {
2501                ordering.reverse()
2502            } else {
2503                ordering
2504            };
2505        }
2506        let ordering = self.name.minor.cmp(&other.name.minor);
2507        if ordering != std::cmp::Ordering::Equal {
2508            return if is_default_request {
2509                ordering.reverse()
2510            } else {
2511                ordering
2512            };
2513        }
2514        let ordering = self.name.patch.cmp(&other.name.patch);
2515        if ordering != std::cmp::Ordering::Equal {
2516            return if is_default_request {
2517                ordering.reverse()
2518            } else {
2519                ordering
2520            };
2521        }
2522        let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2523        if ordering != std::cmp::Ordering::Equal {
2524            return if is_default_request {
2525                ordering.reverse()
2526            } else {
2527                ordering
2528            };
2529        }
2530        let ordering = self.name.variant.cmp(&other.name.variant);
2531        if ordering != std::cmp::Ordering::Equal {
2532            return if is_default_request {
2533                ordering.reverse()
2534            } else {
2535                ordering
2536            };
2537        }
2538        ordering
2539    }
2540}
2541
2542impl PartialOrd for ExecutableNameComparator<'_> {
2543    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2544        Some(self.cmp(other))
2545    }
2546}
2547
2548impl ExecutableName {
2549    #[must_use]
2550    fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2551        self.implementation = Some(implementation);
2552        self
2553    }
2554
2555    #[must_use]
2556    fn with_major(mut self, major: u8) -> Self {
2557        self.major = Some(major);
2558        self
2559    }
2560
2561    #[must_use]
2562    fn with_minor(mut self, minor: u8) -> Self {
2563        self.minor = Some(minor);
2564        self
2565    }
2566
2567    #[must_use]
2568    fn with_patch(mut self, patch: u8) -> Self {
2569        self.patch = Some(patch);
2570        self
2571    }
2572
2573    #[must_use]
2574    fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2575        self.prerelease = Some(prerelease);
2576        self
2577    }
2578
2579    #[must_use]
2580    fn with_variant(mut self, variant: PythonVariant) -> Self {
2581        self.variant = variant;
2582        self
2583    }
2584
2585    fn into_comparator<'a>(
2586        self,
2587        request: &'a VersionRequest,
2588        implementation: Option<&'a ImplementationName>,
2589    ) -> ExecutableNameComparator<'a> {
2590        ExecutableNameComparator {
2591            name: self,
2592            request,
2593            implementation,
2594        }
2595    }
2596}
2597
2598impl fmt::Display for ExecutableName {
2599    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2600        if let Some(implementation) = self.implementation {
2601            write!(f, "{implementation}")?;
2602        } else {
2603            f.write_str("python")?;
2604        }
2605        if let Some(major) = self.major {
2606            write!(f, "{major}")?;
2607            if let Some(minor) = self.minor {
2608                write!(f, ".{minor}")?;
2609                if let Some(patch) = self.patch {
2610                    write!(f, ".{patch}")?;
2611                }
2612            }
2613        }
2614        if let Some(prerelease) = &self.prerelease {
2615            write!(f, "{prerelease}")?;
2616        }
2617        f.write_str(self.variant.executable_suffix())?;
2618        f.write_str(EXE_SUFFIX)?;
2619        Ok(())
2620    }
2621}
2622
2623impl VersionRequest {
2624    /// Drop any patch or prerelease information from the version request.
2625    #[must_use]
2626    pub fn only_minor(self) -> Self {
2627        match self {
2628            Self::Any => self,
2629            Self::Default => self,
2630            Self::Range(specifiers, variant) => Self::Range(
2631                specifiers
2632                    .into_iter()
2633                    .map(|s| s.only_minor_release())
2634                    .collect(),
2635                variant,
2636            ),
2637            Self::Major(..) => self,
2638            Self::MajorMinor(..) => self,
2639            Self::MajorMinorPatch(major, minor, _, variant)
2640            | Self::MajorMinorPrerelease(major, minor, _, variant) => {
2641                Self::MajorMinor(major, minor, variant)
2642            }
2643        }
2644    }
2645
2646    /// Return possible executable names for the given version request.
2647    pub(crate) fn executable_names(
2648        &self,
2649        implementation: Option<&ImplementationName>,
2650    ) -> Vec<ExecutableName> {
2651        let prerelease = if let Self::MajorMinorPrerelease(_, _, prerelease, _) = self {
2652            // Include the prerelease version, e.g., `python3.8a`
2653            Some(prerelease)
2654        } else {
2655            None
2656        };
2657
2658        // Push a default one
2659        let mut names = Vec::new();
2660        names.push(ExecutableName::default());
2661
2662        // Collect each variant depending on the number of versions
2663        if let Some(major) = self.major() {
2664            // e.g. `python3`
2665            names.push(ExecutableName::default().with_major(major));
2666            if let Some(minor) = self.minor() {
2667                // e.g., `python3.12`
2668                names.push(
2669                    ExecutableName::default()
2670                        .with_major(major)
2671                        .with_minor(minor),
2672                );
2673                if let Some(patch) = self.patch() {
2674                    // e.g, `python3.12.1`
2675                    names.push(
2676                        ExecutableName::default()
2677                            .with_major(major)
2678                            .with_minor(minor)
2679                            .with_patch(patch),
2680                    );
2681                }
2682            }
2683        } else {
2684            // Include `3` by default, e.g., `python3`
2685            names.push(ExecutableName::default().with_major(3));
2686        }
2687
2688        if let Some(prerelease) = prerelease {
2689            // Include the prerelease version, e.g., `python3.8a`
2690            for i in 0..names.len() {
2691                let name = names[i];
2692                if name.minor.is_none() {
2693                    // We don't want to include the pre-release marker here
2694                    // e.g. `pythonrc1` and `python3rc1` don't make sense
2695                    continue;
2696                }
2697                names.push(name.with_prerelease(*prerelease));
2698            }
2699        }
2700
2701        // Add all the implementation-specific names
2702        if let Some(implementation) = implementation {
2703            for i in 0..names.len() {
2704                let name = names[i].with_implementation(*implementation);
2705                names.push(name);
2706            }
2707        } else {
2708            // When looking for all implementations, include all possible names
2709            if matches!(self, Self::Any) {
2710                for i in 0..names.len() {
2711                    for implementation in ImplementationName::iter_all() {
2712                        let name = names[i].with_implementation(implementation);
2713                        names.push(name);
2714                    }
2715                }
2716            }
2717        }
2718
2719        // Include free-threaded variants
2720        if let Some(variant) = self.variant() {
2721            if variant != PythonVariant::Default {
2722                for i in 0..names.len() {
2723                    let name = names[i].with_variant(variant);
2724                    names.push(name);
2725                }
2726            }
2727        }
2728
2729        names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
2730        names.reverse();
2731
2732        names
2733    }
2734
2735    /// Return the major version segment of the request, if any.
2736    pub(crate) fn major(&self) -> Option<u8> {
2737        match self {
2738            Self::Any | Self::Default | Self::Range(_, _) => None,
2739            Self::Major(major, _) => Some(*major),
2740            Self::MajorMinor(major, _, _) => Some(*major),
2741            Self::MajorMinorPatch(major, _, _, _) => Some(*major),
2742            Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2743        }
2744    }
2745
2746    /// Return the minor version segment of the request, if any.
2747    pub(crate) fn minor(&self) -> Option<u8> {
2748        match self {
2749            Self::Any | Self::Default | Self::Range(_, _) => None,
2750            Self::Major(_, _) => None,
2751            Self::MajorMinor(_, minor, _) => Some(*minor),
2752            Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
2753            Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2754        }
2755    }
2756
2757    /// Return the patch version segment of the request, if any.
2758    pub(crate) fn patch(&self) -> Option<u8> {
2759        match self {
2760            Self::Any | Self::Default | Self::Range(_, _) => None,
2761            Self::Major(_, _) => None,
2762            Self::MajorMinor(_, _, _) => None,
2763            Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
2764            Self::MajorMinorPrerelease(_, _, _, _) => None,
2765        }
2766    }
2767
2768    /// Return the pre-release segment of the request, if any.
2769    pub(crate) fn prerelease(&self) -> Option<&Prerelease> {
2770        match self {
2771            Self::Any | Self::Default | Self::Range(_, _) => None,
2772            Self::Major(_, _) => None,
2773            Self::MajorMinor(_, _, _) => None,
2774            Self::MajorMinorPatch(_, _, _, _) => None,
2775            Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
2776        }
2777    }
2778
2779    /// Check if the request is for a version supported by uv.
2780    ///
2781    /// If not, an `Err` is returned with an explanatory message.
2782    pub(crate) fn check_supported(&self) -> Result<(), String> {
2783        match self {
2784            Self::Any | Self::Default => (),
2785            Self::Major(major, _) => {
2786                if *major < 3 {
2787                    return Err(format!(
2788                        "Python <3 is not supported but {major} was requested."
2789                    ));
2790                }
2791            }
2792            Self::MajorMinor(major, minor, _) => {
2793                if (*major, *minor) < (3, 6) {
2794                    return Err(format!(
2795                        "Python <3.6 is not supported but {major}.{minor} was requested."
2796                    ));
2797                }
2798            }
2799            Self::MajorMinorPatch(major, minor, patch, _) => {
2800                if (*major, *minor) < (3, 6) {
2801                    return Err(format!(
2802                        "Python <3.6 is not supported but {major}.{minor}.{patch} was requested."
2803                    ));
2804                }
2805            }
2806            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2807                if (*major, *minor) < (3, 6) {
2808                    return Err(format!(
2809                        "Python <3.6 is not supported but {major}.{minor}{prerelease} was requested."
2810                    ));
2811                }
2812            }
2813            // TODO(zanieb): We could do some checking here to see if the range can be satisfied
2814            Self::Range(_, _) => (),
2815        }
2816
2817        if self.is_freethreaded() {
2818            if let Self::MajorMinor(major, minor, _) = self.clone().without_patch() {
2819                if (major, minor) < (3, 13) {
2820                    return Err(format!(
2821                        "Python <3.13 does not support free-threading but {self} was requested."
2822                    ));
2823                }
2824            }
2825        }
2826
2827        Ok(())
2828    }
2829
2830    /// Change this request into a request appropriate for the given [`PythonSource`].
2831    ///
2832    /// For example, if [`VersionRequest::Default`] is requested, it will be changed to
2833    /// [`VersionRequest::Any`] for sources that should allow non-default interpreters like
2834    /// free-threaded variants.
2835    #[must_use]
2836    pub(crate) fn into_request_for_source(self, source: PythonSource) -> Self {
2837        match self {
2838            Self::Default => match source {
2839                PythonSource::ParentInterpreter
2840                | PythonSource::CondaPrefix
2841                | PythonSource::BaseCondaPrefix
2842                | PythonSource::ProvidedPath
2843                | PythonSource::DiscoveredEnvironment
2844                | PythonSource::ActiveEnvironment => Self::Any,
2845                PythonSource::SearchPath
2846                | PythonSource::SearchPathFirst
2847                | PythonSource::Registry
2848                | PythonSource::MicrosoftStore
2849                | PythonSource::Managed => Self::Default,
2850            },
2851            _ => self,
2852        }
2853    }
2854
2855    /// Check if a interpreter matches the request.
2856    pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
2857        match self {
2858            Self::Any => true,
2859            // Do not use free-threaded interpreters by default
2860            Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
2861            Self::Major(major, variant) => {
2862                interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
2863            }
2864            Self::MajorMinor(major, minor, variant) => {
2865                (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
2866                    && variant.matches_interpreter(interpreter)
2867            }
2868            Self::MajorMinorPatch(major, minor, patch, variant) => {
2869                (
2870                    interpreter.python_major(),
2871                    interpreter.python_minor(),
2872                    interpreter.python_patch(),
2873                ) == (*major, *minor, *patch)
2874                    // When a patch version is included, we treat it as a request for a stable
2875                    // release
2876                    && interpreter.python_version().pre().is_none()
2877                    && variant.matches_interpreter(interpreter)
2878            }
2879            Self::Range(specifiers, variant) => {
2880                // If the specifier contains pre-releases, use the full version for comparison.
2881                // Otherwise, strip pre-release so that, e.g., `>=3.14` matches `3.14.0rc3`.
2882                let version = if specifiers
2883                    .iter()
2884                    .any(uv_pep440::VersionSpecifier::any_prerelease)
2885                {
2886                    Cow::Borrowed(interpreter.python_version())
2887                } else {
2888                    Cow::Owned(interpreter.python_version().only_release())
2889                };
2890                specifiers.contains(&version) && variant.matches_interpreter(interpreter)
2891            }
2892            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
2893                let version = interpreter.python_version();
2894                let Some(interpreter_prerelease) = version.pre() else {
2895                    return false;
2896                };
2897                (
2898                    interpreter.python_major(),
2899                    interpreter.python_minor(),
2900                    interpreter_prerelease,
2901                ) == (*major, *minor, *prerelease)
2902                    && variant.matches_interpreter(interpreter)
2903            }
2904        }
2905    }
2906
2907    /// Check if a version is compatible with the request.
2908    ///
2909    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
2910    /// avoid querying interpreters if it's clear it cannot fulfill the request.
2911    pub(crate) fn matches_version(&self, version: &PythonVersion) -> bool {
2912        match self {
2913            Self::Any | Self::Default => true,
2914            Self::Major(major, _) => version.major() == *major,
2915            Self::MajorMinor(major, minor, _) => {
2916                (version.major(), version.minor()) == (*major, *minor)
2917            }
2918            Self::MajorMinorPatch(major, minor, patch, _) => {
2919                (version.major(), version.minor(), version.patch())
2920                    == (*major, *minor, Some(*patch))
2921            }
2922            Self::Range(specifiers, _) => {
2923                // If the specifier contains pre-releases, use the full version for comparison.
2924                // Otherwise, strip pre-release so that, e.g., `>=3.14` matches `3.14.0rc3`.
2925                let version = if specifiers
2926                    .iter()
2927                    .any(uv_pep440::VersionSpecifier::any_prerelease)
2928                {
2929                    Cow::Borrowed(&version.version)
2930                } else {
2931                    Cow::Owned(version.version.only_release())
2932                };
2933                specifiers.contains(&version)
2934            }
2935            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2936                (version.major(), version.minor(), version.pre())
2937                    == (*major, *minor, Some(*prerelease))
2938            }
2939        }
2940    }
2941
2942    /// Check if major and minor version segments are compatible with the request.
2943    ///
2944    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
2945    /// avoid querying interpreters if it's clear it cannot fulfill the request.
2946    fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
2947        match self {
2948            Self::Any | Self::Default => true,
2949            Self::Major(self_major, _) => *self_major == major,
2950            Self::MajorMinor(self_major, self_minor, _) => {
2951                (*self_major, *self_minor) == (major, minor)
2952            }
2953            Self::MajorMinorPatch(self_major, self_minor, _, _) => {
2954                (*self_major, *self_minor) == (major, minor)
2955            }
2956            Self::Range(specifiers, _) => {
2957                let range = release_specifiers_to_ranges(specifiers.clone());
2958                let Some((lower, upper)) = range.bounding_range() else {
2959                    return true;
2960                };
2961                let version = Version::new([u64::from(major), u64::from(minor)]);
2962
2963                let lower = LowerBound::new(lower.cloned());
2964                if !lower.major_minor().contains(&version) {
2965                    return false;
2966                }
2967
2968                let upper = UpperBound::new(upper.cloned());
2969                if !upper.major_minor().contains(&version) {
2970                    return false;
2971                }
2972
2973                true
2974            }
2975            Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
2976                (*self_major, *self_minor) == (major, minor)
2977            }
2978        }
2979    }
2980
2981    /// Check if major, minor, patch, and prerelease version segments are compatible with the
2982    /// request.
2983    ///
2984    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
2985    /// avoid querying interpreters if it's clear it cannot fulfill the request.
2986    pub(crate) fn matches_major_minor_patch_prerelease(
2987        &self,
2988        major: u8,
2989        minor: u8,
2990        patch: u8,
2991        prerelease: Option<Prerelease>,
2992    ) -> bool {
2993        match self {
2994            Self::Any | Self::Default => true,
2995            Self::Major(self_major, _) => *self_major == major,
2996            Self::MajorMinor(self_major, self_minor, _) => {
2997                (*self_major, *self_minor) == (major, minor)
2998            }
2999            Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
3000                (*self_major, *self_minor, *self_patch) == (major, minor, patch)
3001                    // When a patch version is included, we treat it as a request for a stable
3002                    // release
3003                    && prerelease.is_none()
3004            }
3005            Self::Range(specifiers, _) => specifiers.contains(
3006                &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
3007                    .with_pre(prerelease),
3008            ),
3009            Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3010                // Pre-releases of Python versions are always for the zero patch version
3011                (*self_major, *self_minor, 0, Some(*self_prerelease))
3012                    == (major, minor, patch, prerelease)
3013            }
3014        }
3015    }
3016
3017    /// Check if a [`PythonInstallationKey`] is compatible with the request.
3018    ///
3019    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
3020    /// avoid querying interpreters if it's clear it cannot fulfill the request.
3021    pub(crate) fn matches_installation_key(&self, key: &PythonInstallationKey) -> bool {
3022        self.matches_major_minor_patch_prerelease(key.major, key.minor, key.patch, key.prerelease())
3023    }
3024
3025    /// Whether a patch version segment is present in the request.
3026    fn has_patch(&self) -> bool {
3027        match self {
3028            Self::Any | Self::Default => false,
3029            Self::Major(..) => false,
3030            Self::MajorMinor(..) => false,
3031            Self::MajorMinorPatch(..) => true,
3032            Self::MajorMinorPrerelease(..) => false,
3033            Self::Range(_, _) => false,
3034        }
3035    }
3036
3037    /// Return a new [`VersionRequest`] without the patch version if possible.
3038    ///
3039    /// If the patch version is not present, the request is returned unchanged.
3040    #[must_use]
3041    fn without_patch(self) -> Self {
3042        match self {
3043            Self::Default => Self::Default,
3044            Self::Any => Self::Any,
3045            Self::Major(major, variant) => Self::Major(major, variant),
3046            Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3047            Self::MajorMinorPatch(major, minor, _, variant) => {
3048                Self::MajorMinor(major, minor, variant)
3049            }
3050            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3051                Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3052            }
3053            Self::Range(_, _) => self,
3054        }
3055    }
3056
3057    /// Whether this request should allow selection of pre-release versions.
3058    pub(crate) fn allows_prereleases(&self) -> bool {
3059        match self {
3060            Self::Default => false,
3061            Self::Any => true,
3062            Self::Major(..) => false,
3063            Self::MajorMinor(..) => false,
3064            Self::MajorMinorPatch(..) => false,
3065            Self::MajorMinorPrerelease(..) => true,
3066            Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3067        }
3068    }
3069
3070    /// Whether this request is for a debug Python variant.
3071    pub(crate) fn is_debug(&self) -> bool {
3072        match self {
3073            Self::Any | Self::Default => false,
3074            Self::Major(_, variant)
3075            | Self::MajorMinor(_, _, variant)
3076            | Self::MajorMinorPatch(_, _, _, variant)
3077            | Self::MajorMinorPrerelease(_, _, _, variant)
3078            | Self::Range(_, variant) => variant.is_debug(),
3079        }
3080    }
3081
3082    /// Whether this request is for a free-threaded Python variant.
3083    pub(crate) fn is_freethreaded(&self) -> bool {
3084        match self {
3085            Self::Any | Self::Default => false,
3086            Self::Major(_, variant)
3087            | Self::MajorMinor(_, _, variant)
3088            | Self::MajorMinorPatch(_, _, _, variant)
3089            | Self::MajorMinorPrerelease(_, _, _, variant)
3090            | Self::Range(_, variant) => variant.is_freethreaded(),
3091        }
3092    }
3093
3094    /// Return a new [`VersionRequest`] with the [`PythonVariant`] if it has one.
3095    ///
3096    /// This is useful for converting the string representation to pep440.
3097    #[must_use]
3098    pub fn without_python_variant(self) -> Self {
3099        // TODO(zanieb): Replace this entire function with a utility that casts this to a version
3100        // without using `VersionRequest::to_string`.
3101        match self {
3102            Self::Any | Self::Default => self,
3103            Self::Major(major, _) => Self::Major(major, PythonVariant::Default),
3104            Self::MajorMinor(major, minor, _) => {
3105                Self::MajorMinor(major, minor, PythonVariant::Default)
3106            }
3107            Self::MajorMinorPatch(major, minor, patch, _) => {
3108                Self::MajorMinorPatch(major, minor, patch, PythonVariant::Default)
3109            }
3110            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3111                Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Default)
3112            }
3113            Self::Range(specifiers, _) => Self::Range(specifiers, PythonVariant::Default),
3114        }
3115    }
3116
3117    /// Return the [`PythonVariant`] of the request, if any.
3118    pub(crate) fn variant(&self) -> Option<PythonVariant> {
3119        match self {
3120            Self::Any => None,
3121            Self::Default => Some(PythonVariant::Default),
3122            Self::Major(_, variant)
3123            | Self::MajorMinor(_, _, variant)
3124            | Self::MajorMinorPatch(_, _, _, variant)
3125            | Self::MajorMinorPrerelease(_, _, _, variant)
3126            | Self::Range(_, variant) => Some(*variant),
3127        }
3128    }
3129
3130    /// Convert this request into a concrete PEP 440 `Version` when possible.
3131    ///
3132    /// Returns `None` for non-concrete requests
3133    pub fn as_pep440_version(&self) -> Option<Version> {
3134        match self {
3135            Self::Default | Self::Any | Self::Range(_, _) => None,
3136            Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3137            Self::MajorMinor(major, minor, _) => {
3138                Some(Version::new([u64::from(*major), u64::from(*minor)]))
3139            }
3140            Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3141                u64::from(*major),
3142                u64::from(*minor),
3143                u64::from(*patch),
3144            ])),
3145            // Pre-releases of Python versions are always for the zero patch version
3146            Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3147                Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3148            ),
3149        }
3150    }
3151
3152    /// Convert this request into [`VersionSpecifiers`] representing the range of compatible
3153    /// versions.
3154    ///
3155    /// Returns `None` for requests without version constraints (e.g., [`VersionRequest::Default`]
3156    /// and [`VersionRequest::Any`]).
3157    pub fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
3158        match self {
3159            Self::Default | Self::Any => None,
3160            Self::Major(major, _) => Some(VersionSpecifiers::from(
3161                VersionSpecifier::equals_star_version(Version::new([u64::from(*major)])),
3162            )),
3163            Self::MajorMinor(major, minor, _) => Some(VersionSpecifiers::from(
3164                VersionSpecifier::equals_star_version(Version::new([
3165                    u64::from(*major),
3166                    u64::from(*minor),
3167                ])),
3168            )),
3169            Self::MajorMinorPatch(major, minor, patch, _) => {
3170                Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3171                    Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)]),
3172                )))
3173            }
3174            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3175                Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3176                    Version::new([u64::from(*major), u64::from(*minor), 0])
3177                        .with_pre(Some(*prerelease)),
3178                )))
3179            }
3180            Self::Range(specifiers, _) => Some(specifiers.clone()),
3181        }
3182    }
3183}
3184
3185impl FromStr for VersionRequest {
3186    type Err = Error;
3187
3188    fn from_str(s: &str) -> Result<Self, Self::Err> {
3189        /// Extract the variant from the end of a version request string, returning the prefix and
3190        /// the variant type.
3191        fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3192            // This cannot be a valid version, just error immediately
3193            if s.chars().all(char::is_alphabetic) {
3194                return Err(Error::InvalidVersionRequest(s.to_string()));
3195            }
3196
3197            let Some(mut start) = s.rfind(|c: char| c.is_numeric()) else {
3198                return Ok((s, PythonVariant::Default));
3199            };
3200
3201            // Advance past the first digit
3202            start += 1;
3203
3204            // Ensure we're not out of bounds
3205            if start + 1 > s.len() {
3206                return Ok((s, PythonVariant::Default));
3207            }
3208
3209            let variant = &s[start..];
3210            let prefix = &s[..start];
3211
3212            // Strip a leading `+` if present
3213            let variant = variant.strip_prefix('+').unwrap_or(variant);
3214
3215            // TODO(zanieb): Special-case error for use of `dt` instead of `td`
3216
3217            // If there's not a valid variant, fallback to failure in [`Version::from_str`]
3218            let Ok(variant) = PythonVariant::from_str(variant) else {
3219                return Ok((s, PythonVariant::Default));
3220            };
3221
3222            Ok((prefix, variant))
3223        }
3224
3225        let (s, variant) = parse_variant(s)?;
3226        let Ok(version) = Version::from_str(s) else {
3227            return parse_version_specifiers_request(s, variant);
3228        };
3229
3230        // Split the release component if it uses the wheel tag format (e.g., `38`)
3231        let version = split_wheel_tag_release_version(version);
3232
3233        // We dont allow post or dev version here
3234        if version.post().is_some() || version.dev().is_some() {
3235            return Err(Error::InvalidVersionRequest(s.to_string()));
3236        }
3237
3238        // We don't allow local version suffixes unless they're variants, in which case they'd
3239        // already be stripped.
3240        if !version.local().is_empty() {
3241            return Err(Error::InvalidVersionRequest(s.to_string()));
3242        }
3243
3244        // Cast the release components into u8s since that's what we use in `VersionRequest`
3245        let Ok(release) = try_into_u8_slice(&version.release()) else {
3246            return Err(Error::InvalidVersionRequest(s.to_string()));
3247        };
3248
3249        let prerelease = version.pre();
3250
3251        match release.as_slice() {
3252            // e.g. `3
3253            [major] => {
3254                // Prereleases are not allowed here, e.g., `3rc1` doesn't make sense
3255                if prerelease.is_some() {
3256                    return Err(Error::InvalidVersionRequest(s.to_string()));
3257                }
3258                Ok(Self::Major(*major, variant))
3259            }
3260            // e.g. `3.12` or `312` or `3.13rc1`
3261            [major, minor] => {
3262                if let Some(prerelease) = prerelease {
3263                    return Ok(Self::MajorMinorPrerelease(
3264                        *major, *minor, prerelease, variant,
3265                    ));
3266                }
3267                Ok(Self::MajorMinor(*major, *minor, variant))
3268            }
3269            // e.g. `3.12.1` or `3.13.0rc1`
3270            [major, minor, patch] => {
3271                if let Some(prerelease) = prerelease {
3272                    // Prereleases are only allowed for the first patch version, e.g, 3.12.2rc1
3273                    // isn't a proper Python release
3274                    if *patch != 0 {
3275                        return Err(Error::InvalidVersionRequest(s.to_string()));
3276                    }
3277                    return Ok(Self::MajorMinorPrerelease(
3278                        *major, *minor, prerelease, variant,
3279                    ));
3280                }
3281                Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3282            }
3283            _ => Err(Error::InvalidVersionRequest(s.to_string())),
3284        }
3285    }
3286}
3287
3288impl FromStr for PythonVariant {
3289    type Err = ();
3290
3291    fn from_str(s: &str) -> Result<Self, Self::Err> {
3292        match s {
3293            "t" | "freethreaded" => Ok(Self::Freethreaded),
3294            "d" | "debug" => Ok(Self::Debug),
3295            "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3296            "gil" => Ok(Self::Gil),
3297            "gil+debug" => Ok(Self::GilDebug),
3298            "" => Ok(Self::Default),
3299            _ => Err(()),
3300        }
3301    }
3302}
3303
3304impl fmt::Display for PythonVariant {
3305    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3306        match self {
3307            Self::Default => f.write_str("default"),
3308            Self::Debug => f.write_str("debug"),
3309            Self::Freethreaded => f.write_str("freethreaded"),
3310            Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3311            Self::Gil => f.write_str("gil"),
3312            Self::GilDebug => f.write_str("gil+debug"),
3313        }
3314    }
3315}
3316
3317fn parse_version_specifiers_request(
3318    s: &str,
3319    variant: PythonVariant,
3320) -> Result<VersionRequest, Error> {
3321    let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3322        return Err(Error::InvalidVersionRequest(s.to_string()));
3323    };
3324    if specifiers.is_empty() {
3325        return Err(Error::InvalidVersionRequest(s.to_string()));
3326    }
3327    Ok(VersionRequest::Range(specifiers, variant))
3328}
3329
3330impl From<&PythonVersion> for VersionRequest {
3331    fn from(version: &PythonVersion) -> Self {
3332        Self::from_str(&version.string)
3333            .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3334    }
3335}
3336
3337impl fmt::Display for VersionRequest {
3338    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3339        match self {
3340            Self::Any => f.write_str("any"),
3341            Self::Default => f.write_str("default"),
3342            Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3343            Self::MajorMinor(major, minor, variant) => {
3344                write!(f, "{major}.{minor}{}", variant.display_suffix())
3345            }
3346            Self::MajorMinorPatch(major, minor, patch, variant) => {
3347                write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3348            }
3349            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3350                write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3351            }
3352            Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3353        }
3354    }
3355}
3356
3357impl fmt::Display for PythonRequest {
3358    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3359        match self {
3360            Self::Default => write!(f, "a default Python"),
3361            Self::Any => write!(f, "any Python"),
3362            Self::Version(version) => write!(f, "Python {version}"),
3363            Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3364            Self::File(path) => write!(f, "path `{}`", path.user_display()),
3365            Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3366            Self::Implementation(implementation) => {
3367                write!(f, "{}", implementation.pretty())
3368            }
3369            Self::ImplementationVersion(implementation, version) => {
3370                write!(f, "{} {version}", implementation.pretty())
3371            }
3372            Self::Key(request) => write!(f, "{request}"),
3373        }
3374    }
3375}
3376
3377impl fmt::Display for PythonSource {
3378    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3379        match self {
3380            Self::ProvidedPath => f.write_str("provided path"),
3381            Self::ActiveEnvironment => f.write_str("active virtual environment"),
3382            Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3383            Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3384            Self::SearchPath => f.write_str("search path"),
3385            Self::SearchPathFirst => f.write_str("first executable in the search path"),
3386            Self::Registry => f.write_str("registry"),
3387            Self::MicrosoftStore => f.write_str("Microsoft Store"),
3388            Self::Managed => f.write_str("managed installations"),
3389            Self::ParentInterpreter => f.write_str("parent interpreter"),
3390        }
3391    }
3392}
3393
3394impl PythonPreference {
3395    /// Return the sources that are considered when searching for a Python interpreter with this
3396    /// preference.
3397    fn sources(self) -> &'static [PythonSource] {
3398        match self {
3399            Self::OnlyManaged => &[PythonSource::Managed],
3400            Self::Managed => {
3401                if cfg!(windows) {
3402                    &[
3403                        PythonSource::Managed,
3404                        PythonSource::SearchPath,
3405                        PythonSource::Registry,
3406                    ]
3407                } else {
3408                    &[PythonSource::Managed, PythonSource::SearchPath]
3409                }
3410            }
3411            Self::System => {
3412                if cfg!(windows) {
3413                    &[
3414                        PythonSource::SearchPath,
3415                        PythonSource::Registry,
3416                        PythonSource::Managed,
3417                    ]
3418                } else {
3419                    &[PythonSource::SearchPath, PythonSource::Managed]
3420                }
3421            }
3422            Self::OnlySystem => {
3423                if cfg!(windows) {
3424                    &[PythonSource::SearchPath, PythonSource::Registry]
3425                } else {
3426                    &[PythonSource::SearchPath]
3427                }
3428            }
3429        }
3430    }
3431
3432    /// Return the canonical name.
3433    // TODO(zanieb): This should be a `Display` impl and we should have a different view for
3434    // the sources
3435    pub fn canonical_name(&self) -> &'static str {
3436        match self {
3437            Self::OnlyManaged => "only managed",
3438            Self::Managed => "prefer managed",
3439            Self::System => "prefer system",
3440            Self::OnlySystem => "only system",
3441        }
3442    }
3443}
3444
3445impl fmt::Display for PythonPreference {
3446    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3447        f.write_str(match self {
3448            Self::OnlyManaged => "only managed",
3449            Self::Managed => "prefer managed",
3450            Self::System => "prefer system",
3451            Self::OnlySystem => "only system",
3452        })
3453    }
3454}
3455
3456impl DiscoveryPreferences {
3457    /// Return a string describing the sources that are considered when searching for Python with
3458    /// the given preferences.
3459    fn sources(&self, request: &PythonRequest) -> String {
3460        let python_sources = self
3461            .python_preference
3462            .sources()
3463            .iter()
3464            .map(ToString::to_string)
3465            .collect::<Vec<_>>();
3466        match self.environment_preference {
3467            EnvironmentPreference::Any => disjunction(
3468                &["virtual environments"]
3469                    .into_iter()
3470                    .chain(python_sources.iter().map(String::as_str))
3471                    .collect::<Vec<_>>(),
3472            ),
3473            EnvironmentPreference::ExplicitSystem => {
3474                if request.is_explicit_system() {
3475                    disjunction(
3476                        &["virtual environments"]
3477                            .into_iter()
3478                            .chain(python_sources.iter().map(String::as_str))
3479                            .collect::<Vec<_>>(),
3480                    )
3481                } else {
3482                    disjunction(&["virtual environments"])
3483                }
3484            }
3485            EnvironmentPreference::OnlySystem => disjunction(
3486                &python_sources
3487                    .iter()
3488                    .map(String::as_str)
3489                    .collect::<Vec<_>>(),
3490            ),
3491            EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3492        }
3493    }
3494}
3495
3496impl fmt::Display for PythonNotFound {
3497    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3498        let sources = DiscoveryPreferences {
3499            python_preference: self.python_preference,
3500            environment_preference: self.environment_preference,
3501        }
3502        .sources(&self.request);
3503
3504        match self.request {
3505            PythonRequest::Default | PythonRequest::Any => {
3506                write!(f, "No interpreter found in {sources}")
3507            }
3508            PythonRequest::File(_) => {
3509                write!(f, "No interpreter found at {}", self.request)
3510            }
3511            PythonRequest::Directory(_) => {
3512                write!(f, "No interpreter found in {}", self.request)
3513            }
3514            _ => {
3515                write!(f, "No interpreter found for {} in {sources}", self.request)
3516            }
3517        }
3518    }
3519}
3520
3521/// Join a series of items with `or` separators, making use of commas when necessary.
3522fn disjunction(items: &[&str]) -> String {
3523    match items.len() {
3524        0 => String::new(),
3525        1 => items[0].to_string(),
3526        2 => format!("{} or {}", items[0], items[1]),
3527        _ => {
3528            let last = items.last().unwrap();
3529            format!(
3530                "{}, or {}",
3531                items.iter().take(items.len() - 1).join(", "),
3532                last
3533            )
3534        }
3535    }
3536}
3537
3538fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3539    release
3540        .iter()
3541        .map(|x| match u8::try_from(*x) {
3542            Ok(x) => Ok(x),
3543            Err(e) => Err(e),
3544        })
3545        .collect()
3546}
3547
3548/// Convert a wheel tag formatted version (e.g., `38`) to multiple components (e.g., `3.8`).
3549///
3550/// The major version is always assumed to be a single digit 0-9. The minor version is all
3551/// the following content.
3552///
3553/// If not a wheel tag formatted version, the input is returned unchanged.
3554fn split_wheel_tag_release_version(version: Version) -> Version {
3555    let release = version.release();
3556    if release.len() != 1 {
3557        return version;
3558    }
3559
3560    let release = release[0].to_string();
3561    let mut chars = release.chars();
3562    let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3563        return version;
3564    };
3565
3566    let Ok(minor) = chars.as_str().parse::<u32>() else {
3567        return version;
3568    };
3569
3570    version.with_release([u64::from(major), u64::from(minor)])
3571}
3572
3573#[cfg(test)]
3574mod tests {
3575    use std::{path::PathBuf, str::FromStr};
3576
3577    use assert_fs::{TempDir, prelude::*};
3578    use target_lexicon::{Aarch64Architecture, Architecture};
3579    use test_log::test;
3580    use uv_distribution_types::RequiresPython;
3581    use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3582
3583    use crate::{
3584        discovery::{PythonRequest, VersionRequest},
3585        downloads::{ArchRequest, PythonDownloadRequest},
3586        implementation::ImplementationName,
3587    };
3588    use uv_platform::{Arch, Libc, Os};
3589
3590    use super::{
3591        DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonVariant,
3592    };
3593
3594    #[test]
3595    fn interpreter_request_from_str() {
3596        assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3597        assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
3598        assert_eq!(
3599            PythonRequest::parse("3.12"),
3600            PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
3601        );
3602        assert_eq!(
3603            PythonRequest::parse(">=3.12"),
3604            PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3605        );
3606        assert_eq!(
3607            PythonRequest::parse(">=3.12,<3.13"),
3608            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3609        );
3610        assert_eq!(
3611            PythonRequest::parse(">=3.12,<3.13"),
3612            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3613        );
3614
3615        assert_eq!(
3616            PythonRequest::parse("3.13.0a1"),
3617            PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3618        );
3619        assert_eq!(
3620            PythonRequest::parse("3.13.0b5"),
3621            PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3622        );
3623        assert_eq!(
3624            PythonRequest::parse("3.13.0rc1"),
3625            PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3626        );
3627        assert_eq!(
3628            PythonRequest::parse("3.13.1rc1"),
3629            PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3630            "Pre-release version requests require a patch version of zero"
3631        );
3632        assert_eq!(
3633            PythonRequest::parse("3rc1"),
3634            PythonRequest::ExecutableName("3rc1".to_string()),
3635            "Pre-release version requests require a minor version"
3636        );
3637
3638        assert_eq!(
3639            PythonRequest::parse("cpython"),
3640            PythonRequest::Implementation(ImplementationName::CPython)
3641        );
3642
3643        assert_eq!(
3644            PythonRequest::parse("cpython3.12.2"),
3645            PythonRequest::ImplementationVersion(
3646                ImplementationName::CPython,
3647                VersionRequest::from_str("3.12.2").unwrap(),
3648            )
3649        );
3650
3651        assert_eq!(
3652            PythonRequest::parse("cpython-3.13.2"),
3653            PythonRequest::Key(PythonDownloadRequest {
3654                version: Some(VersionRequest::MajorMinorPatch(
3655                    3,
3656                    13,
3657                    2,
3658                    PythonVariant::Default
3659                )),
3660                implementation: Some(ImplementationName::CPython),
3661                arch: None,
3662                os: None,
3663                libc: None,
3664                build: None,
3665                prereleases: None
3666            })
3667        );
3668        assert_eq!(
3669            PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3670            PythonRequest::Key(PythonDownloadRequest {
3671                version: Some(VersionRequest::MajorMinorPatch(
3672                    3,
3673                    13,
3674                    2,
3675                    PythonVariant::Default
3676                )),
3677                implementation: Some(ImplementationName::CPython),
3678                arch: Some(ArchRequest::Explicit(Arch::new(
3679                    Architecture::Aarch64(Aarch64Architecture::Aarch64),
3680                    None
3681                ))),
3682                os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3683                libc: Some(Libc::None),
3684                build: None,
3685                prereleases: None
3686            })
3687        );
3688        assert_eq!(
3689            PythonRequest::parse("any-3.13.2"),
3690            PythonRequest::Key(PythonDownloadRequest {
3691                version: Some(VersionRequest::MajorMinorPatch(
3692                    3,
3693                    13,
3694                    2,
3695                    PythonVariant::Default
3696                )),
3697                implementation: None,
3698                arch: None,
3699                os: None,
3700                libc: None,
3701                build: None,
3702                prereleases: None
3703            })
3704        );
3705        assert_eq!(
3706            PythonRequest::parse("any-3.13.2-any-aarch64"),
3707            PythonRequest::Key(PythonDownloadRequest {
3708                version: Some(VersionRequest::MajorMinorPatch(
3709                    3,
3710                    13,
3711                    2,
3712                    PythonVariant::Default
3713                )),
3714                implementation: None,
3715                arch: Some(ArchRequest::Explicit(Arch::new(
3716                    Architecture::Aarch64(Aarch64Architecture::Aarch64),
3717                    None
3718                ))),
3719                os: None,
3720                libc: None,
3721                build: None,
3722                prereleases: None
3723            })
3724        );
3725
3726        assert_eq!(
3727            PythonRequest::parse("pypy"),
3728            PythonRequest::Implementation(ImplementationName::PyPy)
3729        );
3730        assert_eq!(
3731            PythonRequest::parse("pp"),
3732            PythonRequest::Implementation(ImplementationName::PyPy)
3733        );
3734        assert_eq!(
3735            PythonRequest::parse("graalpy"),
3736            PythonRequest::Implementation(ImplementationName::GraalPy)
3737        );
3738        assert_eq!(
3739            PythonRequest::parse("gp"),
3740            PythonRequest::Implementation(ImplementationName::GraalPy)
3741        );
3742        assert_eq!(
3743            PythonRequest::parse("cp"),
3744            PythonRequest::Implementation(ImplementationName::CPython)
3745        );
3746        assert_eq!(
3747            PythonRequest::parse("pypy3.10"),
3748            PythonRequest::ImplementationVersion(
3749                ImplementationName::PyPy,
3750                VersionRequest::from_str("3.10").unwrap(),
3751            )
3752        );
3753        assert_eq!(
3754            PythonRequest::parse("pp310"),
3755            PythonRequest::ImplementationVersion(
3756                ImplementationName::PyPy,
3757                VersionRequest::from_str("3.10").unwrap(),
3758            )
3759        );
3760        assert_eq!(
3761            PythonRequest::parse("graalpy3.10"),
3762            PythonRequest::ImplementationVersion(
3763                ImplementationName::GraalPy,
3764                VersionRequest::from_str("3.10").unwrap(),
3765            )
3766        );
3767        assert_eq!(
3768            PythonRequest::parse("gp310"),
3769            PythonRequest::ImplementationVersion(
3770                ImplementationName::GraalPy,
3771                VersionRequest::from_str("3.10").unwrap(),
3772            )
3773        );
3774        assert_eq!(
3775            PythonRequest::parse("cp38"),
3776            PythonRequest::ImplementationVersion(
3777                ImplementationName::CPython,
3778                VersionRequest::from_str("3.8").unwrap(),
3779            )
3780        );
3781        assert_eq!(
3782            PythonRequest::parse("pypy@3.10"),
3783            PythonRequest::ImplementationVersion(
3784                ImplementationName::PyPy,
3785                VersionRequest::from_str("3.10").unwrap(),
3786            )
3787        );
3788        assert_eq!(
3789            PythonRequest::parse("pypy310"),
3790            PythonRequest::ImplementationVersion(
3791                ImplementationName::PyPy,
3792                VersionRequest::from_str("3.10").unwrap(),
3793            )
3794        );
3795        assert_eq!(
3796            PythonRequest::parse("graalpy@3.10"),
3797            PythonRequest::ImplementationVersion(
3798                ImplementationName::GraalPy,
3799                VersionRequest::from_str("3.10").unwrap(),
3800            )
3801        );
3802        assert_eq!(
3803            PythonRequest::parse("graalpy310"),
3804            PythonRequest::ImplementationVersion(
3805                ImplementationName::GraalPy,
3806                VersionRequest::from_str("3.10").unwrap(),
3807            )
3808        );
3809
3810        let tempdir = TempDir::new().unwrap();
3811        assert_eq!(
3812            PythonRequest::parse(tempdir.path().to_str().unwrap()),
3813            PythonRequest::Directory(tempdir.path().to_path_buf()),
3814            "An existing directory is treated as a directory"
3815        );
3816        assert_eq!(
3817            PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3818            PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3819            "A path that does not exist is treated as a file"
3820        );
3821        tempdir.child("bar").touch().unwrap();
3822        assert_eq!(
3823            PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3824            PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3825            "An existing file is treated as a file"
3826        );
3827        assert_eq!(
3828            PythonRequest::parse("./foo"),
3829            PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
3830            "A string with a file system separator is treated as a file"
3831        );
3832        assert_eq!(
3833            PythonRequest::parse("3.13t"),
3834            PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
3835        );
3836    }
3837
3838    #[test]
3839    fn discovery_sources_prefer_system_orders_search_path_first() {
3840        let preferences = DiscoveryPreferences {
3841            python_preference: PythonPreference::System,
3842            environment_preference: EnvironmentPreference::OnlySystem,
3843        };
3844        let sources = preferences.sources(&PythonRequest::Default);
3845
3846        if cfg!(windows) {
3847            assert_eq!(sources, "search path, registry, or managed installations");
3848        } else {
3849            assert_eq!(sources, "search path or managed installations");
3850        }
3851    }
3852
3853    #[test]
3854    fn discovery_sources_only_system_matches_platform_order() {
3855        let preferences = DiscoveryPreferences {
3856            python_preference: PythonPreference::OnlySystem,
3857            environment_preference: EnvironmentPreference::OnlySystem,
3858        };
3859        let sources = preferences.sources(&PythonRequest::Default);
3860
3861        if cfg!(windows) {
3862            assert_eq!(sources, "search path or registry");
3863        } else {
3864            assert_eq!(sources, "search path");
3865        }
3866    }
3867
3868    #[test]
3869    fn interpreter_request_to_canonical_string() {
3870        assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
3871        assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
3872        assert_eq!(
3873            PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
3874            "3.12"
3875        );
3876        assert_eq!(
3877            PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3878                .to_canonical_string(),
3879            ">=3.12"
3880        );
3881        assert_eq!(
3882            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3883                .to_canonical_string(),
3884            ">=3.12, <3.13"
3885        );
3886
3887        assert_eq!(
3888            PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3889                .to_canonical_string(),
3890            "3.13a1"
3891        );
3892
3893        assert_eq!(
3894            PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3895                .to_canonical_string(),
3896            "3.13b5"
3897        );
3898
3899        assert_eq!(
3900            PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3901                .to_canonical_string(),
3902            "3.13rc1"
3903        );
3904
3905        assert_eq!(
3906            PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
3907                .to_canonical_string(),
3908            "3.13rc4"
3909        );
3910
3911        assert_eq!(
3912            PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
3913            "foo"
3914        );
3915        assert_eq!(
3916            PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
3917            "cpython"
3918        );
3919        assert_eq!(
3920            PythonRequest::ImplementationVersion(
3921                ImplementationName::CPython,
3922                VersionRequest::from_str("3.12.2").unwrap(),
3923            )
3924            .to_canonical_string(),
3925            "cpython@3.12.2"
3926        );
3927        assert_eq!(
3928            PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
3929            "pypy"
3930        );
3931        assert_eq!(
3932            PythonRequest::ImplementationVersion(
3933                ImplementationName::PyPy,
3934                VersionRequest::from_str("3.10").unwrap(),
3935            )
3936            .to_canonical_string(),
3937            "pypy@3.10"
3938        );
3939        assert_eq!(
3940            PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
3941            "graalpy"
3942        );
3943        assert_eq!(
3944            PythonRequest::ImplementationVersion(
3945                ImplementationName::GraalPy,
3946                VersionRequest::from_str("3.10").unwrap(),
3947            )
3948            .to_canonical_string(),
3949            "graalpy@3.10"
3950        );
3951
3952        let tempdir = TempDir::new().unwrap();
3953        assert_eq!(
3954            PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
3955            tempdir.path().to_str().unwrap(),
3956            "An existing directory is treated as a directory"
3957        );
3958        assert_eq!(
3959            PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
3960            tempdir.child("foo").path().to_str().unwrap(),
3961            "A path that does not exist is treated as a file"
3962        );
3963        tempdir.child("bar").touch().unwrap();
3964        assert_eq!(
3965            PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
3966            tempdir.child("bar").path().to_str().unwrap(),
3967            "An existing file is treated as a file"
3968        );
3969        assert_eq!(
3970            PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
3971            "./foo",
3972            "A string with a file system separator is treated as a file"
3973        );
3974    }
3975
3976    #[test]
3977    fn version_request_from_str() {
3978        assert_eq!(
3979            VersionRequest::from_str("3").unwrap(),
3980            VersionRequest::Major(3, PythonVariant::Default)
3981        );
3982        assert_eq!(
3983            VersionRequest::from_str("3.12").unwrap(),
3984            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
3985        );
3986        assert_eq!(
3987            VersionRequest::from_str("3.12.1").unwrap(),
3988            VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
3989        );
3990        assert!(VersionRequest::from_str("1.foo.1").is_err());
3991        assert_eq!(
3992            VersionRequest::from_str("3").unwrap(),
3993            VersionRequest::Major(3, PythonVariant::Default)
3994        );
3995        assert_eq!(
3996            VersionRequest::from_str("38").unwrap(),
3997            VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
3998        );
3999        assert_eq!(
4000            VersionRequest::from_str("312").unwrap(),
4001            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4002        );
4003        assert_eq!(
4004            VersionRequest::from_str("3100").unwrap(),
4005            VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
4006        );
4007        assert_eq!(
4008            VersionRequest::from_str("3.13a1").unwrap(),
4009            VersionRequest::MajorMinorPrerelease(
4010                3,
4011                13,
4012                Prerelease {
4013                    kind: PrereleaseKind::Alpha,
4014                    number: 1
4015                },
4016                PythonVariant::Default
4017            )
4018        );
4019        assert_eq!(
4020            VersionRequest::from_str("313b1").unwrap(),
4021            VersionRequest::MajorMinorPrerelease(
4022                3,
4023                13,
4024                Prerelease {
4025                    kind: PrereleaseKind::Beta,
4026                    number: 1
4027                },
4028                PythonVariant::Default
4029            )
4030        );
4031        assert_eq!(
4032            VersionRequest::from_str("3.13.0b2").unwrap(),
4033            VersionRequest::MajorMinorPrerelease(
4034                3,
4035                13,
4036                Prerelease {
4037                    kind: PrereleaseKind::Beta,
4038                    number: 2
4039                },
4040                PythonVariant::Default
4041            )
4042        );
4043        assert_eq!(
4044            VersionRequest::from_str("3.13.0rc3").unwrap(),
4045            VersionRequest::MajorMinorPrerelease(
4046                3,
4047                13,
4048                Prerelease {
4049                    kind: PrereleaseKind::Rc,
4050                    number: 3
4051                },
4052                PythonVariant::Default
4053            )
4054        );
4055        assert!(
4056            matches!(
4057                VersionRequest::from_str("3rc1"),
4058                Err(Error::InvalidVersionRequest(_))
4059            ),
4060            "Pre-release version requests require a minor version"
4061        );
4062        assert!(
4063            matches!(
4064                VersionRequest::from_str("3.13.2rc1"),
4065                Err(Error::InvalidVersionRequest(_))
4066            ),
4067            "Pre-release version requests require a patch version of zero"
4068        );
4069        assert!(
4070            matches!(
4071                VersionRequest::from_str("3.12-dev"),
4072                Err(Error::InvalidVersionRequest(_))
4073            ),
4074            "Development version segments are not allowed"
4075        );
4076        assert!(
4077            matches!(
4078                VersionRequest::from_str("3.12+local"),
4079                Err(Error::InvalidVersionRequest(_))
4080            ),
4081            "Local version segments are not allowed"
4082        );
4083        assert!(
4084            matches!(
4085                VersionRequest::from_str("3.12.post0"),
4086                Err(Error::InvalidVersionRequest(_))
4087            ),
4088            "Post version segments are not allowed"
4089        );
4090        assert!(
4091            // Test for overflow
4092            matches!(
4093                VersionRequest::from_str("31000"),
4094                Err(Error::InvalidVersionRequest(_))
4095            )
4096        );
4097        assert_eq!(
4098            VersionRequest::from_str("3t").unwrap(),
4099            VersionRequest::Major(3, PythonVariant::Freethreaded)
4100        );
4101        assert_eq!(
4102            VersionRequest::from_str("313t").unwrap(),
4103            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4104        );
4105        assert_eq!(
4106            VersionRequest::from_str("3.13t").unwrap(),
4107            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4108        );
4109        assert_eq!(
4110            VersionRequest::from_str(">=3.13t").unwrap(),
4111            VersionRequest::Range(
4112                VersionSpecifiers::from_str(">=3.13").unwrap(),
4113                PythonVariant::Freethreaded
4114            )
4115        );
4116        assert_eq!(
4117            VersionRequest::from_str(">=3.13").unwrap(),
4118            VersionRequest::Range(
4119                VersionSpecifiers::from_str(">=3.13").unwrap(),
4120                PythonVariant::Default
4121            )
4122        );
4123        assert_eq!(
4124            VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4125            VersionRequest::Range(
4126                VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4127                PythonVariant::Freethreaded
4128            )
4129        );
4130        assert!(matches!(
4131            VersionRequest::from_str("3.13tt"),
4132            Err(Error::InvalidVersionRequest(_))
4133        ));
4134    }
4135
4136    #[test]
4137    fn executable_names_from_request() {
4138        fn case(request: &str, expected: &[&str]) {
4139            let (implementation, version) = match PythonRequest::parse(request) {
4140                PythonRequest::Any => (None, VersionRequest::Any),
4141                PythonRequest::Default => (None, VersionRequest::Default),
4142                PythonRequest::Version(version) => (None, version),
4143                PythonRequest::ImplementationVersion(implementation, version) => {
4144                    (Some(implementation), version)
4145                }
4146                PythonRequest::Implementation(implementation) => {
4147                    (Some(implementation), VersionRequest::Default)
4148                }
4149                result => {
4150                    panic!("Test cases should request versions or implementations; got {result:?}")
4151                }
4152            };
4153
4154            let result: Vec<_> = version
4155                .executable_names(implementation.as_ref())
4156                .into_iter()
4157                .map(|name| name.to_string())
4158                .collect();
4159
4160            let expected: Vec<_> = expected
4161                .iter()
4162                .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4163                .collect();
4164
4165            assert_eq!(result, expected, "mismatch for case \"{request}\"");
4166        }
4167
4168        case(
4169            "any",
4170            &[
4171                "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4172                "pyodide", "pyodide3",
4173            ],
4174        );
4175
4176        case("default", &["python", "python3"]);
4177
4178        case("3", &["python3", "python"]);
4179
4180        case("4", &["python4", "python"]);
4181
4182        case("3.13", &["python3.13", "python3", "python"]);
4183
4184        case("pypy", &["pypy", "pypy3", "python", "python3"]);
4185
4186        case(
4187            "pypy@3.10",
4188            &[
4189                "pypy3.10",
4190                "pypy3",
4191                "pypy",
4192                "python3.10",
4193                "python3",
4194                "python",
4195            ],
4196        );
4197
4198        case(
4199            "3.13t",
4200            &[
4201                "python3.13t",
4202                "python3.13",
4203                "python3t",
4204                "python3",
4205                "pythont",
4206                "python",
4207            ],
4208        );
4209        case("3t", &["python3t", "python3", "pythont", "python"]);
4210
4211        case(
4212            "3.13.2",
4213            &["python3.13.2", "python3.13", "python3", "python"],
4214        );
4215
4216        case(
4217            "3.13rc2",
4218            &["python3.13rc2", "python3.13", "python3", "python"],
4219        );
4220    }
4221
4222    #[test]
4223    fn test_try_split_prefix_and_version() {
4224        assert!(matches!(
4225            PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4226            Ok(None),
4227        ));
4228        assert!(matches!(
4229            PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4230            Ok(Some(_)),
4231        ));
4232        assert!(matches!(
4233            PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4234            Ok(Some(_)),
4235        ));
4236        assert!(matches!(
4237            PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4238            Ok(None),
4239        ));
4240        // Version parsing errors are only raised if @ is present.
4241        assert!(
4242            PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4243        );
4244        // @ is not allowed if the prefix is empty.
4245        assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4246    }
4247
4248    #[test]
4249    fn version_request_as_pep440_version() {
4250        // Non-concrete requests return `None`
4251        assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4252        assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4253        assert_eq!(
4254            VersionRequest::from_str(">=3.10")
4255                .unwrap()
4256                .as_pep440_version(),
4257            None
4258        );
4259
4260        // `VersionRequest::Major`
4261        assert_eq!(
4262            VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4263            Some(Version::from_str("3").unwrap())
4264        );
4265
4266        // `VersionRequest::MajorMinor`
4267        assert_eq!(
4268            VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4269            Some(Version::from_str("3.12").unwrap())
4270        );
4271
4272        // `VersionRequest::MajorMinorPatch`
4273        assert_eq!(
4274            VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4275            Some(Version::from_str("3.12.5").unwrap())
4276        );
4277
4278        // `VersionRequest::MajorMinorPrerelease`
4279        assert_eq!(
4280            VersionRequest::MajorMinorPrerelease(
4281                3,
4282                14,
4283                Prerelease {
4284                    kind: PrereleaseKind::Alpha,
4285                    number: 1
4286                },
4287                PythonVariant::Default
4288            )
4289            .as_pep440_version(),
4290            Some(Version::from_str("3.14.0a1").unwrap())
4291        );
4292        assert_eq!(
4293            VersionRequest::MajorMinorPrerelease(
4294                3,
4295                14,
4296                Prerelease {
4297                    kind: PrereleaseKind::Beta,
4298                    number: 2
4299                },
4300                PythonVariant::Default
4301            )
4302            .as_pep440_version(),
4303            Some(Version::from_str("3.14.0b2").unwrap())
4304        );
4305        assert_eq!(
4306            VersionRequest::MajorMinorPrerelease(
4307                3,
4308                13,
4309                Prerelease {
4310                    kind: PrereleaseKind::Rc,
4311                    number: 3
4312                },
4313                PythonVariant::Default
4314            )
4315            .as_pep440_version(),
4316            Some(Version::from_str("3.13.0rc3").unwrap())
4317        );
4318
4319        // Variant is ignored
4320        assert_eq!(
4321            VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4322            Some(Version::from_str("3").unwrap())
4323        );
4324        assert_eq!(
4325            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4326            Some(Version::from_str("3.13").unwrap())
4327        );
4328    }
4329
4330    #[test]
4331    fn python_request_as_pep440_version() {
4332        // `PythonRequest::Any` and `PythonRequest::Default` return `None`
4333        assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4334        assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4335
4336        // `PythonRequest::Version` delegates to `VersionRequest`
4337        assert_eq!(
4338            PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4339                .as_pep440_version(),
4340            Some(Version::from_str("3.11").unwrap())
4341        );
4342
4343        // `PythonRequest::ImplementationVersion` extracts version
4344        assert_eq!(
4345            PythonRequest::ImplementationVersion(
4346                ImplementationName::CPython,
4347                VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4348            )
4349            .as_pep440_version(),
4350            Some(Version::from_str("3.12.1").unwrap())
4351        );
4352
4353        // `PythonRequest::Implementation` returns `None` (no version)
4354        assert_eq!(
4355            PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4356            None
4357        );
4358
4359        // `PythonRequest::Key` with version
4360        assert_eq!(
4361            PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4362            Some(Version::from_str("3.13.2").unwrap())
4363        );
4364
4365        // `PythonRequest::Key` without version returns `None`
4366        assert_eq!(
4367            PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4368            None
4369        );
4370
4371        // Range versions return `None`
4372        assert_eq!(
4373            PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4374            None
4375        );
4376    }
4377
4378    #[test]
4379    fn intersects_requires_python_exact() {
4380        let requires_python =
4381            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4382
4383        assert!(PythonRequest::parse("3.12").intersects_requires_python(&requires_python));
4384        assert!(!PythonRequest::parse("3.11").intersects_requires_python(&requires_python));
4385    }
4386
4387    #[test]
4388    fn intersects_requires_python_major() {
4389        let requires_python =
4390            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4391
4392        // `3` overlaps with `>=3.12` (e.g., 3.12, 3.13, ... are all Python 3)
4393        assert!(PythonRequest::parse("3").intersects_requires_python(&requires_python));
4394        // `2` does not overlap with `>=3.12`
4395        assert!(!PythonRequest::parse("2").intersects_requires_python(&requires_python));
4396    }
4397
4398    #[test]
4399    fn intersects_requires_python_range() {
4400        let requires_python =
4401            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4402
4403        assert!(PythonRequest::parse(">=3.12,<3.13").intersects_requires_python(&requires_python));
4404        assert!(!PythonRequest::parse(">=3.10,<3.12").intersects_requires_python(&requires_python));
4405    }
4406
4407    #[test]
4408    fn intersects_requires_python_implementation_range() {
4409        let requires_python =
4410            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4411
4412        assert!(
4413            PythonRequest::parse("cpython@>=3.12,<3.13")
4414                .intersects_requires_python(&requires_python)
4415        );
4416        assert!(
4417            !PythonRequest::parse("cpython@>=3.10,<3.12")
4418                .intersects_requires_python(&requires_python)
4419        );
4420    }
4421
4422    #[test]
4423    fn intersects_requires_python_no_version() {
4424        let requires_python =
4425            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4426
4427        // Requests without version constraints are always compatible
4428        assert!(PythonRequest::Any.intersects_requires_python(&requires_python));
4429        assert!(PythonRequest::Default.intersects_requires_python(&requires_python));
4430        assert!(
4431            PythonRequest::Implementation(ImplementationName::CPython)
4432                .intersects_requires_python(&requires_python)
4433        );
4434    }
4435}