Skip to main content

uv_python/
discovery.rs

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