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