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