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;
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    /// Whether a patch version segment is present in the request.
2979    fn has_patch(&self) -> bool {
2980        match self {
2981            Self::Any | Self::Default => false,
2982            Self::Major(..) => false,
2983            Self::MajorMinor(..) => false,
2984            Self::MajorMinorPatch(..) => true,
2985            Self::MajorMinorPrerelease(..) => false,
2986            Self::Range(_, _) => false,
2987        }
2988    }
2989
2990    /// Return a new [`VersionRequest`] without the patch version if possible.
2991    ///
2992    /// If the patch version is not present, the request is returned unchanged.
2993    #[must_use]
2994    fn without_patch(self) -> Self {
2995        match self {
2996            Self::Default => Self::Default,
2997            Self::Any => Self::Any,
2998            Self::Major(major, variant) => Self::Major(major, variant),
2999            Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3000            Self::MajorMinorPatch(major, minor, _, variant) => {
3001                Self::MajorMinor(major, minor, variant)
3002            }
3003            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3004                Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3005            }
3006            Self::Range(_, _) => self,
3007        }
3008    }
3009
3010    /// Whether this request should allow selection of pre-release versions.
3011    pub(crate) fn allows_prereleases(&self) -> bool {
3012        match self {
3013            Self::Default => false,
3014            Self::Any => true,
3015            Self::Major(..) => false,
3016            Self::MajorMinor(..) => false,
3017            Self::MajorMinorPatch(..) => false,
3018            Self::MajorMinorPrerelease(..) => true,
3019            Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3020        }
3021    }
3022
3023    /// Whether this request is for a debug Python variant.
3024    pub(crate) fn is_debug(&self) -> bool {
3025        match self {
3026            Self::Any | Self::Default => false,
3027            Self::Major(_, variant)
3028            | Self::MajorMinor(_, _, variant)
3029            | Self::MajorMinorPatch(_, _, _, variant)
3030            | Self::MajorMinorPrerelease(_, _, _, variant)
3031            | Self::Range(_, variant) => variant.is_debug(),
3032        }
3033    }
3034
3035    /// Whether this request is for a free-threaded Python variant.
3036    pub(crate) fn is_freethreaded(&self) -> bool {
3037        match self {
3038            Self::Any | Self::Default => false,
3039            Self::Major(_, variant)
3040            | Self::MajorMinor(_, _, variant)
3041            | Self::MajorMinorPatch(_, _, _, variant)
3042            | Self::MajorMinorPrerelease(_, _, _, variant)
3043            | Self::Range(_, variant) => variant.is_freethreaded(),
3044        }
3045    }
3046
3047    /// Return a new [`VersionRequest`] with the [`PythonVariant`] if it has one.
3048    ///
3049    /// This is useful for converting the string representation to pep440.
3050    #[must_use]
3051    pub fn without_python_variant(self) -> Self {
3052        // TODO(zanieb): Replace this entire function with a utility that casts this to a version
3053        // without using `VersionRequest::to_string`.
3054        match self {
3055            Self::Any | Self::Default => self,
3056            Self::Major(major, _) => Self::Major(major, PythonVariant::Default),
3057            Self::MajorMinor(major, minor, _) => {
3058                Self::MajorMinor(major, minor, PythonVariant::Default)
3059            }
3060            Self::MajorMinorPatch(major, minor, patch, _) => {
3061                Self::MajorMinorPatch(major, minor, patch, PythonVariant::Default)
3062            }
3063            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3064                Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Default)
3065            }
3066            Self::Range(specifiers, _) => Self::Range(specifiers, PythonVariant::Default),
3067        }
3068    }
3069
3070    /// Return the [`PythonVariant`] of the request, if any.
3071    pub(crate) fn variant(&self) -> Option<PythonVariant> {
3072        match self {
3073            Self::Any => None,
3074            Self::Default => Some(PythonVariant::Default),
3075            Self::Major(_, variant)
3076            | Self::MajorMinor(_, _, variant)
3077            | Self::MajorMinorPatch(_, _, _, variant)
3078            | Self::MajorMinorPrerelease(_, _, _, variant)
3079            | Self::Range(_, variant) => Some(*variant),
3080        }
3081    }
3082
3083    /// Convert this request into a concrete PEP 440 `Version` when possible.
3084    ///
3085    /// Returns `None` for non-concrete requests
3086    pub fn as_pep440_version(&self) -> Option<Version> {
3087        match self {
3088            Self::Default | Self::Any | Self::Range(_, _) => None,
3089            Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3090            Self::MajorMinor(major, minor, _) => {
3091                Some(Version::new([u64::from(*major), u64::from(*minor)]))
3092            }
3093            Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3094                u64::from(*major),
3095                u64::from(*minor),
3096                u64::from(*patch),
3097            ])),
3098            // Pre-releases of Python versions are always for the zero patch version
3099            Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3100                Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3101            ),
3102        }
3103    }
3104}
3105
3106impl FromStr for VersionRequest {
3107    type Err = Error;
3108
3109    fn from_str(s: &str) -> Result<Self, Self::Err> {
3110        /// Extract the variant from the end of a version request string, returning the prefix and
3111        /// the variant type.
3112        fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3113            // This cannot be a valid version, just error immediately
3114            if s.chars().all(char::is_alphabetic) {
3115                return Err(Error::InvalidVersionRequest(s.to_string()));
3116            }
3117
3118            let Some(mut start) = s.rfind(|c: char| c.is_numeric()) else {
3119                return Ok((s, PythonVariant::Default));
3120            };
3121
3122            // Advance past the first digit
3123            start += 1;
3124
3125            // Ensure we're not out of bounds
3126            if start + 1 > s.len() {
3127                return Ok((s, PythonVariant::Default));
3128            }
3129
3130            let variant = &s[start..];
3131            let prefix = &s[..start];
3132
3133            // Strip a leading `+` if present
3134            let variant = variant.strip_prefix('+').unwrap_or(variant);
3135
3136            // TODO(zanieb): Special-case error for use of `dt` instead of `td`
3137
3138            // If there's not a valid variant, fallback to failure in [`Version::from_str`]
3139            let Ok(variant) = PythonVariant::from_str(variant) else {
3140                return Ok((s, PythonVariant::Default));
3141            };
3142
3143            Ok((prefix, variant))
3144        }
3145
3146        let (s, variant) = parse_variant(s)?;
3147        let Ok(version) = Version::from_str(s) else {
3148            return parse_version_specifiers_request(s, variant);
3149        };
3150
3151        // Split the release component if it uses the wheel tag format (e.g., `38`)
3152        let version = split_wheel_tag_release_version(version);
3153
3154        // We dont allow post or dev version here
3155        if version.post().is_some() || version.dev().is_some() {
3156            return Err(Error::InvalidVersionRequest(s.to_string()));
3157        }
3158
3159        // We don't allow local version suffixes unless they're variants, in which case they'd
3160        // already be stripped.
3161        if !version.local().is_empty() {
3162            return Err(Error::InvalidVersionRequest(s.to_string()));
3163        }
3164
3165        // Cast the release components into u8s since that's what we use in `VersionRequest`
3166        let Ok(release) = try_into_u8_slice(&version.release()) else {
3167            return Err(Error::InvalidVersionRequest(s.to_string()));
3168        };
3169
3170        let prerelease = version.pre();
3171
3172        match release.as_slice() {
3173            // e.g. `3
3174            [major] => {
3175                // Prereleases are not allowed here, e.g., `3rc1` doesn't make sense
3176                if prerelease.is_some() {
3177                    return Err(Error::InvalidVersionRequest(s.to_string()));
3178                }
3179                Ok(Self::Major(*major, variant))
3180            }
3181            // e.g. `3.12` or `312` or `3.13rc1`
3182            [major, minor] => {
3183                if let Some(prerelease) = prerelease {
3184                    return Ok(Self::MajorMinorPrerelease(
3185                        *major, *minor, prerelease, variant,
3186                    ));
3187                }
3188                Ok(Self::MajorMinor(*major, *minor, variant))
3189            }
3190            // e.g. `3.12.1` or `3.13.0rc1`
3191            [major, minor, patch] => {
3192                if let Some(prerelease) = prerelease {
3193                    // Prereleases are only allowed for the first patch version, e.g, 3.12.2rc1
3194                    // isn't a proper Python release
3195                    if *patch != 0 {
3196                        return Err(Error::InvalidVersionRequest(s.to_string()));
3197                    }
3198                    return Ok(Self::MajorMinorPrerelease(
3199                        *major, *minor, prerelease, variant,
3200                    ));
3201                }
3202                Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3203            }
3204            _ => Err(Error::InvalidVersionRequest(s.to_string())),
3205        }
3206    }
3207}
3208
3209impl FromStr for PythonVariant {
3210    type Err = ();
3211
3212    fn from_str(s: &str) -> Result<Self, Self::Err> {
3213        match s {
3214            "t" | "freethreaded" => Ok(Self::Freethreaded),
3215            "d" | "debug" => Ok(Self::Debug),
3216            "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3217            "gil" => Ok(Self::Gil),
3218            "gil+debug" => Ok(Self::GilDebug),
3219            "" => Ok(Self::Default),
3220            _ => Err(()),
3221        }
3222    }
3223}
3224
3225impl fmt::Display for PythonVariant {
3226    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3227        match self {
3228            Self::Default => f.write_str("default"),
3229            Self::Debug => f.write_str("debug"),
3230            Self::Freethreaded => f.write_str("freethreaded"),
3231            Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3232            Self::Gil => f.write_str("gil"),
3233            Self::GilDebug => f.write_str("gil+debug"),
3234        }
3235    }
3236}
3237
3238fn parse_version_specifiers_request(
3239    s: &str,
3240    variant: PythonVariant,
3241) -> Result<VersionRequest, Error> {
3242    let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3243        return Err(Error::InvalidVersionRequest(s.to_string()));
3244    };
3245    if specifiers.is_empty() {
3246        return Err(Error::InvalidVersionRequest(s.to_string()));
3247    }
3248    Ok(VersionRequest::Range(specifiers, variant))
3249}
3250
3251impl From<&PythonVersion> for VersionRequest {
3252    fn from(version: &PythonVersion) -> Self {
3253        Self::from_str(&version.string)
3254            .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3255    }
3256}
3257
3258impl fmt::Display for VersionRequest {
3259    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3260        match self {
3261            Self::Any => f.write_str("any"),
3262            Self::Default => f.write_str("default"),
3263            Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3264            Self::MajorMinor(major, minor, variant) => {
3265                write!(f, "{major}.{minor}{}", variant.display_suffix())
3266            }
3267            Self::MajorMinorPatch(major, minor, patch, variant) => {
3268                write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3269            }
3270            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3271                write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3272            }
3273            Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3274        }
3275    }
3276}
3277
3278impl fmt::Display for PythonRequest {
3279    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3280        match self {
3281            Self::Default => write!(f, "a default Python"),
3282            Self::Any => write!(f, "any Python"),
3283            Self::Version(version) => write!(f, "Python {version}"),
3284            Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3285            Self::File(path) => write!(f, "path `{}`", path.user_display()),
3286            Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3287            Self::Implementation(implementation) => {
3288                write!(f, "{}", implementation.pretty())
3289            }
3290            Self::ImplementationVersion(implementation, version) => {
3291                write!(f, "{} {version}", implementation.pretty())
3292            }
3293            Self::Key(request) => write!(f, "{request}"),
3294        }
3295    }
3296}
3297
3298impl fmt::Display for PythonSource {
3299    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3300        match self {
3301            Self::ProvidedPath => f.write_str("provided path"),
3302            Self::ActiveEnvironment => f.write_str("active virtual environment"),
3303            Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3304            Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3305            Self::SearchPath => f.write_str("search path"),
3306            Self::SearchPathFirst => f.write_str("first executable in the search path"),
3307            Self::Registry => f.write_str("registry"),
3308            Self::MicrosoftStore => f.write_str("Microsoft Store"),
3309            Self::Managed => f.write_str("managed installations"),
3310            Self::ParentInterpreter => f.write_str("parent interpreter"),
3311        }
3312    }
3313}
3314
3315impl PythonPreference {
3316    /// Return the sources that are considered when searching for a Python interpreter with this
3317    /// preference.
3318    fn sources(self) -> &'static [PythonSource] {
3319        match self {
3320            Self::OnlyManaged => &[PythonSource::Managed],
3321            Self::Managed => {
3322                if cfg!(windows) {
3323                    &[
3324                        PythonSource::Managed,
3325                        PythonSource::SearchPath,
3326                        PythonSource::Registry,
3327                    ]
3328                } else {
3329                    &[PythonSource::Managed, PythonSource::SearchPath]
3330                }
3331            }
3332            Self::System => {
3333                if cfg!(windows) {
3334                    &[
3335                        PythonSource::SearchPath,
3336                        PythonSource::Registry,
3337                        PythonSource::Managed,
3338                    ]
3339                } else {
3340                    &[PythonSource::SearchPath, PythonSource::Managed]
3341                }
3342            }
3343            Self::OnlySystem => {
3344                if cfg!(windows) {
3345                    &[PythonSource::SearchPath, PythonSource::Registry]
3346                } else {
3347                    &[PythonSource::SearchPath]
3348                }
3349            }
3350        }
3351    }
3352
3353    /// Return the canonical name.
3354    // TODO(zanieb): This should be a `Display` impl and we should have a different view for
3355    // the sources
3356    pub fn canonical_name(&self) -> &'static str {
3357        match self {
3358            Self::OnlyManaged => "only managed",
3359            Self::Managed => "prefer managed",
3360            Self::System => "prefer system",
3361            Self::OnlySystem => "only system",
3362        }
3363    }
3364}
3365
3366impl fmt::Display for PythonPreference {
3367    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3368        f.write_str(match self {
3369            Self::OnlyManaged => "only managed",
3370            Self::Managed => "prefer managed",
3371            Self::System => "prefer system",
3372            Self::OnlySystem => "only system",
3373        })
3374    }
3375}
3376
3377impl DiscoveryPreferences {
3378    /// Return a string describing the sources that are considered when searching for Python with
3379    /// the given preferences.
3380    fn sources(&self, request: &PythonRequest) -> String {
3381        let python_sources = self
3382            .python_preference
3383            .sources()
3384            .iter()
3385            .map(ToString::to_string)
3386            .collect::<Vec<_>>();
3387        match self.environment_preference {
3388            EnvironmentPreference::Any => disjunction(
3389                &["virtual environments"]
3390                    .into_iter()
3391                    .chain(python_sources.iter().map(String::as_str))
3392                    .collect::<Vec<_>>(),
3393            ),
3394            EnvironmentPreference::ExplicitSystem => {
3395                if request.is_explicit_system() {
3396                    disjunction(
3397                        &["virtual environments"]
3398                            .into_iter()
3399                            .chain(python_sources.iter().map(String::as_str))
3400                            .collect::<Vec<_>>(),
3401                    )
3402                } else {
3403                    disjunction(&["virtual environments"])
3404                }
3405            }
3406            EnvironmentPreference::OnlySystem => disjunction(
3407                &python_sources
3408                    .iter()
3409                    .map(String::as_str)
3410                    .collect::<Vec<_>>(),
3411            ),
3412            EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3413        }
3414    }
3415}
3416
3417impl fmt::Display for PythonNotFound {
3418    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3419        let sources = DiscoveryPreferences {
3420            python_preference: self.python_preference,
3421            environment_preference: self.environment_preference,
3422        }
3423        .sources(&self.request);
3424
3425        match self.request {
3426            PythonRequest::Default | PythonRequest::Any => {
3427                write!(f, "No interpreter found in {sources}")
3428            }
3429            PythonRequest::File(_) => {
3430                write!(f, "No interpreter found at {}", self.request)
3431            }
3432            PythonRequest::Directory(_) => {
3433                write!(f, "No interpreter found in {}", self.request)
3434            }
3435            _ => {
3436                write!(f, "No interpreter found for {} in {sources}", self.request)
3437            }
3438        }
3439    }
3440}
3441
3442/// Join a series of items with `or` separators, making use of commas when necessary.
3443fn disjunction(items: &[&str]) -> String {
3444    match items.len() {
3445        0 => String::new(),
3446        1 => items[0].to_string(),
3447        2 => format!("{} or {}", items[0], items[1]),
3448        _ => {
3449            let last = items.last().unwrap();
3450            format!(
3451                "{}, or {}",
3452                items.iter().take(items.len() - 1).join(", "),
3453                last
3454            )
3455        }
3456    }
3457}
3458
3459fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3460    release
3461        .iter()
3462        .map(|x| match u8::try_from(*x) {
3463            Ok(x) => Ok(x),
3464            Err(e) => Err(e),
3465        })
3466        .collect()
3467}
3468
3469/// Convert a wheel tag formatted version (e.g., `38`) to multiple components (e.g., `3.8`).
3470///
3471/// The major version is always assumed to be a single digit 0-9. The minor version is all
3472/// the following content.
3473///
3474/// If not a wheel tag formatted version, the input is returned unchanged.
3475fn split_wheel_tag_release_version(version: Version) -> Version {
3476    let release = version.release();
3477    if release.len() != 1 {
3478        return version;
3479    }
3480
3481    let release = release[0].to_string();
3482    let mut chars = release.chars();
3483    let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3484        return version;
3485    };
3486
3487    let Ok(minor) = chars.as_str().parse::<u32>() else {
3488        return version;
3489    };
3490
3491    version.with_release([u64::from(major), u64::from(minor)])
3492}
3493
3494#[cfg(test)]
3495mod tests {
3496    use std::{path::PathBuf, str::FromStr};
3497
3498    use assert_fs::{TempDir, prelude::*};
3499    use target_lexicon::{Aarch64Architecture, Architecture};
3500    use test_log::test;
3501    use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3502
3503    use crate::{
3504        discovery::{PythonRequest, VersionRequest},
3505        downloads::{ArchRequest, PythonDownloadRequest},
3506        implementation::ImplementationName,
3507    };
3508    use uv_platform::{Arch, Libc, Os};
3509
3510    use super::{
3511        DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonVariant,
3512    };
3513
3514    #[test]
3515    fn interpreter_request_from_str() {
3516        assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3517        assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
3518        assert_eq!(
3519            PythonRequest::parse("3.12"),
3520            PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
3521        );
3522        assert_eq!(
3523            PythonRequest::parse(">=3.12"),
3524            PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3525        );
3526        assert_eq!(
3527            PythonRequest::parse(">=3.12,<3.13"),
3528            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3529        );
3530        assert_eq!(
3531            PythonRequest::parse(">=3.12,<3.13"),
3532            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3533        );
3534
3535        assert_eq!(
3536            PythonRequest::parse("3.13.0a1"),
3537            PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3538        );
3539        assert_eq!(
3540            PythonRequest::parse("3.13.0b5"),
3541            PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3542        );
3543        assert_eq!(
3544            PythonRequest::parse("3.13.0rc1"),
3545            PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3546        );
3547        assert_eq!(
3548            PythonRequest::parse("3.13.1rc1"),
3549            PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3550            "Pre-release version requests require a patch version of zero"
3551        );
3552        assert_eq!(
3553            PythonRequest::parse("3rc1"),
3554            PythonRequest::ExecutableName("3rc1".to_string()),
3555            "Pre-release version requests require a minor version"
3556        );
3557
3558        assert_eq!(
3559            PythonRequest::parse("cpython"),
3560            PythonRequest::Implementation(ImplementationName::CPython)
3561        );
3562
3563        assert_eq!(
3564            PythonRequest::parse("cpython3.12.2"),
3565            PythonRequest::ImplementationVersion(
3566                ImplementationName::CPython,
3567                VersionRequest::from_str("3.12.2").unwrap(),
3568            )
3569        );
3570
3571        assert_eq!(
3572            PythonRequest::parse("cpython-3.13.2"),
3573            PythonRequest::Key(PythonDownloadRequest {
3574                version: Some(VersionRequest::MajorMinorPatch(
3575                    3,
3576                    13,
3577                    2,
3578                    PythonVariant::Default
3579                )),
3580                implementation: Some(ImplementationName::CPython),
3581                arch: None,
3582                os: None,
3583                libc: None,
3584                build: None,
3585                prereleases: None
3586            })
3587        );
3588        assert_eq!(
3589            PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3590            PythonRequest::Key(PythonDownloadRequest {
3591                version: Some(VersionRequest::MajorMinorPatch(
3592                    3,
3593                    13,
3594                    2,
3595                    PythonVariant::Default
3596                )),
3597                implementation: Some(ImplementationName::CPython),
3598                arch: Some(ArchRequest::Explicit(Arch::new(
3599                    Architecture::Aarch64(Aarch64Architecture::Aarch64),
3600                    None
3601                ))),
3602                os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3603                libc: Some(Libc::None),
3604                build: None,
3605                prereleases: None
3606            })
3607        );
3608        assert_eq!(
3609            PythonRequest::parse("any-3.13.2"),
3610            PythonRequest::Key(PythonDownloadRequest {
3611                version: Some(VersionRequest::MajorMinorPatch(
3612                    3,
3613                    13,
3614                    2,
3615                    PythonVariant::Default
3616                )),
3617                implementation: None,
3618                arch: None,
3619                os: None,
3620                libc: None,
3621                build: None,
3622                prereleases: None
3623            })
3624        );
3625        assert_eq!(
3626            PythonRequest::parse("any-3.13.2-any-aarch64"),
3627            PythonRequest::Key(PythonDownloadRequest {
3628                version: Some(VersionRequest::MajorMinorPatch(
3629                    3,
3630                    13,
3631                    2,
3632                    PythonVariant::Default
3633                )),
3634                implementation: None,
3635                arch: Some(ArchRequest::Explicit(Arch::new(
3636                    Architecture::Aarch64(Aarch64Architecture::Aarch64),
3637                    None
3638                ))),
3639                os: None,
3640                libc: None,
3641                build: None,
3642                prereleases: None
3643            })
3644        );
3645
3646        assert_eq!(
3647            PythonRequest::parse("pypy"),
3648            PythonRequest::Implementation(ImplementationName::PyPy)
3649        );
3650        assert_eq!(
3651            PythonRequest::parse("pp"),
3652            PythonRequest::Implementation(ImplementationName::PyPy)
3653        );
3654        assert_eq!(
3655            PythonRequest::parse("graalpy"),
3656            PythonRequest::Implementation(ImplementationName::GraalPy)
3657        );
3658        assert_eq!(
3659            PythonRequest::parse("gp"),
3660            PythonRequest::Implementation(ImplementationName::GraalPy)
3661        );
3662        assert_eq!(
3663            PythonRequest::parse("cp"),
3664            PythonRequest::Implementation(ImplementationName::CPython)
3665        );
3666        assert_eq!(
3667            PythonRequest::parse("pypy3.10"),
3668            PythonRequest::ImplementationVersion(
3669                ImplementationName::PyPy,
3670                VersionRequest::from_str("3.10").unwrap(),
3671            )
3672        );
3673        assert_eq!(
3674            PythonRequest::parse("pp310"),
3675            PythonRequest::ImplementationVersion(
3676                ImplementationName::PyPy,
3677                VersionRequest::from_str("3.10").unwrap(),
3678            )
3679        );
3680        assert_eq!(
3681            PythonRequest::parse("graalpy3.10"),
3682            PythonRequest::ImplementationVersion(
3683                ImplementationName::GraalPy,
3684                VersionRequest::from_str("3.10").unwrap(),
3685            )
3686        );
3687        assert_eq!(
3688            PythonRequest::parse("gp310"),
3689            PythonRequest::ImplementationVersion(
3690                ImplementationName::GraalPy,
3691                VersionRequest::from_str("3.10").unwrap(),
3692            )
3693        );
3694        assert_eq!(
3695            PythonRequest::parse("cp38"),
3696            PythonRequest::ImplementationVersion(
3697                ImplementationName::CPython,
3698                VersionRequest::from_str("3.8").unwrap(),
3699            )
3700        );
3701        assert_eq!(
3702            PythonRequest::parse("pypy@3.10"),
3703            PythonRequest::ImplementationVersion(
3704                ImplementationName::PyPy,
3705                VersionRequest::from_str("3.10").unwrap(),
3706            )
3707        );
3708        assert_eq!(
3709            PythonRequest::parse("pypy310"),
3710            PythonRequest::ImplementationVersion(
3711                ImplementationName::PyPy,
3712                VersionRequest::from_str("3.10").unwrap(),
3713            )
3714        );
3715        assert_eq!(
3716            PythonRequest::parse("graalpy@3.10"),
3717            PythonRequest::ImplementationVersion(
3718                ImplementationName::GraalPy,
3719                VersionRequest::from_str("3.10").unwrap(),
3720            )
3721        );
3722        assert_eq!(
3723            PythonRequest::parse("graalpy310"),
3724            PythonRequest::ImplementationVersion(
3725                ImplementationName::GraalPy,
3726                VersionRequest::from_str("3.10").unwrap(),
3727            )
3728        );
3729
3730        let tempdir = TempDir::new().unwrap();
3731        assert_eq!(
3732            PythonRequest::parse(tempdir.path().to_str().unwrap()),
3733            PythonRequest::Directory(tempdir.path().to_path_buf()),
3734            "An existing directory is treated as a directory"
3735        );
3736        assert_eq!(
3737            PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3738            PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3739            "A path that does not exist is treated as a file"
3740        );
3741        tempdir.child("bar").touch().unwrap();
3742        assert_eq!(
3743            PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3744            PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3745            "An existing file is treated as a file"
3746        );
3747        assert_eq!(
3748            PythonRequest::parse("./foo"),
3749            PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
3750            "A string with a file system separator is treated as a file"
3751        );
3752        assert_eq!(
3753            PythonRequest::parse("3.13t"),
3754            PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
3755        );
3756    }
3757
3758    #[test]
3759    fn discovery_sources_prefer_system_orders_search_path_first() {
3760        let preferences = DiscoveryPreferences {
3761            python_preference: PythonPreference::System,
3762            environment_preference: EnvironmentPreference::OnlySystem,
3763        };
3764        let sources = preferences.sources(&PythonRequest::Default);
3765
3766        if cfg!(windows) {
3767            assert_eq!(sources, "search path, registry, or managed installations");
3768        } else {
3769            assert_eq!(sources, "search path or managed installations");
3770        }
3771    }
3772
3773    #[test]
3774    fn discovery_sources_only_system_matches_platform_order() {
3775        let preferences = DiscoveryPreferences {
3776            python_preference: PythonPreference::OnlySystem,
3777            environment_preference: EnvironmentPreference::OnlySystem,
3778        };
3779        let sources = preferences.sources(&PythonRequest::Default);
3780
3781        if cfg!(windows) {
3782            assert_eq!(sources, "search path or registry");
3783        } else {
3784            assert_eq!(sources, "search path");
3785        }
3786    }
3787
3788    #[test]
3789    fn interpreter_request_to_canonical_string() {
3790        assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
3791        assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
3792        assert_eq!(
3793            PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
3794            "3.12"
3795        );
3796        assert_eq!(
3797            PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3798                .to_canonical_string(),
3799            ">=3.12"
3800        );
3801        assert_eq!(
3802            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3803                .to_canonical_string(),
3804            ">=3.12, <3.13"
3805        );
3806
3807        assert_eq!(
3808            PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3809                .to_canonical_string(),
3810            "3.13a1"
3811        );
3812
3813        assert_eq!(
3814            PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3815                .to_canonical_string(),
3816            "3.13b5"
3817        );
3818
3819        assert_eq!(
3820            PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3821                .to_canonical_string(),
3822            "3.13rc1"
3823        );
3824
3825        assert_eq!(
3826            PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
3827                .to_canonical_string(),
3828            "3.13rc4"
3829        );
3830
3831        assert_eq!(
3832            PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
3833            "foo"
3834        );
3835        assert_eq!(
3836            PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
3837            "cpython"
3838        );
3839        assert_eq!(
3840            PythonRequest::ImplementationVersion(
3841                ImplementationName::CPython,
3842                VersionRequest::from_str("3.12.2").unwrap(),
3843            )
3844            .to_canonical_string(),
3845            "cpython@3.12.2"
3846        );
3847        assert_eq!(
3848            PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
3849            "pypy"
3850        );
3851        assert_eq!(
3852            PythonRequest::ImplementationVersion(
3853                ImplementationName::PyPy,
3854                VersionRequest::from_str("3.10").unwrap(),
3855            )
3856            .to_canonical_string(),
3857            "pypy@3.10"
3858        );
3859        assert_eq!(
3860            PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
3861            "graalpy"
3862        );
3863        assert_eq!(
3864            PythonRequest::ImplementationVersion(
3865                ImplementationName::GraalPy,
3866                VersionRequest::from_str("3.10").unwrap(),
3867            )
3868            .to_canonical_string(),
3869            "graalpy@3.10"
3870        );
3871
3872        let tempdir = TempDir::new().unwrap();
3873        assert_eq!(
3874            PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
3875            tempdir.path().to_str().unwrap(),
3876            "An existing directory is treated as a directory"
3877        );
3878        assert_eq!(
3879            PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
3880            tempdir.child("foo").path().to_str().unwrap(),
3881            "A path that does not exist is treated as a file"
3882        );
3883        tempdir.child("bar").touch().unwrap();
3884        assert_eq!(
3885            PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
3886            tempdir.child("bar").path().to_str().unwrap(),
3887            "An existing file is treated as a file"
3888        );
3889        assert_eq!(
3890            PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
3891            "./foo",
3892            "A string with a file system separator is treated as a file"
3893        );
3894    }
3895
3896    #[test]
3897    fn version_request_from_str() {
3898        assert_eq!(
3899            VersionRequest::from_str("3").unwrap(),
3900            VersionRequest::Major(3, PythonVariant::Default)
3901        );
3902        assert_eq!(
3903            VersionRequest::from_str("3.12").unwrap(),
3904            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
3905        );
3906        assert_eq!(
3907            VersionRequest::from_str("3.12.1").unwrap(),
3908            VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
3909        );
3910        assert!(VersionRequest::from_str("1.foo.1").is_err());
3911        assert_eq!(
3912            VersionRequest::from_str("3").unwrap(),
3913            VersionRequest::Major(3, PythonVariant::Default)
3914        );
3915        assert_eq!(
3916            VersionRequest::from_str("38").unwrap(),
3917            VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
3918        );
3919        assert_eq!(
3920            VersionRequest::from_str("312").unwrap(),
3921            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
3922        );
3923        assert_eq!(
3924            VersionRequest::from_str("3100").unwrap(),
3925            VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
3926        );
3927        assert_eq!(
3928            VersionRequest::from_str("3.13a1").unwrap(),
3929            VersionRequest::MajorMinorPrerelease(
3930                3,
3931                13,
3932                Prerelease {
3933                    kind: PrereleaseKind::Alpha,
3934                    number: 1
3935                },
3936                PythonVariant::Default
3937            )
3938        );
3939        assert_eq!(
3940            VersionRequest::from_str("313b1").unwrap(),
3941            VersionRequest::MajorMinorPrerelease(
3942                3,
3943                13,
3944                Prerelease {
3945                    kind: PrereleaseKind::Beta,
3946                    number: 1
3947                },
3948                PythonVariant::Default
3949            )
3950        );
3951        assert_eq!(
3952            VersionRequest::from_str("3.13.0b2").unwrap(),
3953            VersionRequest::MajorMinorPrerelease(
3954                3,
3955                13,
3956                Prerelease {
3957                    kind: PrereleaseKind::Beta,
3958                    number: 2
3959                },
3960                PythonVariant::Default
3961            )
3962        );
3963        assert_eq!(
3964            VersionRequest::from_str("3.13.0rc3").unwrap(),
3965            VersionRequest::MajorMinorPrerelease(
3966                3,
3967                13,
3968                Prerelease {
3969                    kind: PrereleaseKind::Rc,
3970                    number: 3
3971                },
3972                PythonVariant::Default
3973            )
3974        );
3975        assert!(
3976            matches!(
3977                VersionRequest::from_str("3rc1"),
3978                Err(Error::InvalidVersionRequest(_))
3979            ),
3980            "Pre-release version requests require a minor version"
3981        );
3982        assert!(
3983            matches!(
3984                VersionRequest::from_str("3.13.2rc1"),
3985                Err(Error::InvalidVersionRequest(_))
3986            ),
3987            "Pre-release version requests require a patch version of zero"
3988        );
3989        assert!(
3990            matches!(
3991                VersionRequest::from_str("3.12-dev"),
3992                Err(Error::InvalidVersionRequest(_))
3993            ),
3994            "Development version segments are not allowed"
3995        );
3996        assert!(
3997            matches!(
3998                VersionRequest::from_str("3.12+local"),
3999                Err(Error::InvalidVersionRequest(_))
4000            ),
4001            "Local version segments are not allowed"
4002        );
4003        assert!(
4004            matches!(
4005                VersionRequest::from_str("3.12.post0"),
4006                Err(Error::InvalidVersionRequest(_))
4007            ),
4008            "Post version segments are not allowed"
4009        );
4010        assert!(
4011            // Test for overflow
4012            matches!(
4013                VersionRequest::from_str("31000"),
4014                Err(Error::InvalidVersionRequest(_))
4015            )
4016        );
4017        assert_eq!(
4018            VersionRequest::from_str("3t").unwrap(),
4019            VersionRequest::Major(3, PythonVariant::Freethreaded)
4020        );
4021        assert_eq!(
4022            VersionRequest::from_str("313t").unwrap(),
4023            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4024        );
4025        assert_eq!(
4026            VersionRequest::from_str("3.13t").unwrap(),
4027            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4028        );
4029        assert_eq!(
4030            VersionRequest::from_str(">=3.13t").unwrap(),
4031            VersionRequest::Range(
4032                VersionSpecifiers::from_str(">=3.13").unwrap(),
4033                PythonVariant::Freethreaded
4034            )
4035        );
4036        assert_eq!(
4037            VersionRequest::from_str(">=3.13").unwrap(),
4038            VersionRequest::Range(
4039                VersionSpecifiers::from_str(">=3.13").unwrap(),
4040                PythonVariant::Default
4041            )
4042        );
4043        assert_eq!(
4044            VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4045            VersionRequest::Range(
4046                VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4047                PythonVariant::Freethreaded
4048            )
4049        );
4050        assert!(matches!(
4051            VersionRequest::from_str("3.13tt"),
4052            Err(Error::InvalidVersionRequest(_))
4053        ));
4054    }
4055
4056    #[test]
4057    fn executable_names_from_request() {
4058        fn case(request: &str, expected: &[&str]) {
4059            let (implementation, version) = match PythonRequest::parse(request) {
4060                PythonRequest::Any => (None, VersionRequest::Any),
4061                PythonRequest::Default => (None, VersionRequest::Default),
4062                PythonRequest::Version(version) => (None, version),
4063                PythonRequest::ImplementationVersion(implementation, version) => {
4064                    (Some(implementation), version)
4065                }
4066                PythonRequest::Implementation(implementation) => {
4067                    (Some(implementation), VersionRequest::Default)
4068                }
4069                result => {
4070                    panic!("Test cases should request versions or implementations; got {result:?}")
4071                }
4072            };
4073
4074            let result: Vec<_> = version
4075                .executable_names(implementation.as_ref())
4076                .into_iter()
4077                .map(|name| name.to_string())
4078                .collect();
4079
4080            let expected: Vec<_> = expected
4081                .iter()
4082                .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4083                .collect();
4084
4085            assert_eq!(result, expected, "mismatch for case \"{request}\"");
4086        }
4087
4088        case(
4089            "any",
4090            &[
4091                "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4092                "pyodide", "pyodide3",
4093            ],
4094        );
4095
4096        case("default", &["python", "python3"]);
4097
4098        case("3", &["python3", "python"]);
4099
4100        case("4", &["python4", "python"]);
4101
4102        case("3.13", &["python3.13", "python3", "python"]);
4103
4104        case("pypy", &["pypy", "pypy3", "python", "python3"]);
4105
4106        case(
4107            "pypy@3.10",
4108            &[
4109                "pypy3.10",
4110                "pypy3",
4111                "pypy",
4112                "python3.10",
4113                "python3",
4114                "python",
4115            ],
4116        );
4117
4118        case(
4119            "3.13t",
4120            &[
4121                "python3.13t",
4122                "python3.13",
4123                "python3t",
4124                "python3",
4125                "pythont",
4126                "python",
4127            ],
4128        );
4129        case("3t", &["python3t", "python3", "pythont", "python"]);
4130
4131        case(
4132            "3.13.2",
4133            &["python3.13.2", "python3.13", "python3", "python"],
4134        );
4135
4136        case(
4137            "3.13rc2",
4138            &["python3.13rc2", "python3.13", "python3", "python"],
4139        );
4140    }
4141
4142    #[test]
4143    fn test_try_split_prefix_and_version() {
4144        assert!(matches!(
4145            PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4146            Ok(None),
4147        ));
4148        assert!(matches!(
4149            PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4150            Ok(Some(_)),
4151        ));
4152        assert!(matches!(
4153            PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4154            Ok(Some(_)),
4155        ));
4156        assert!(matches!(
4157            PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4158            Ok(None),
4159        ));
4160        // Version parsing errors are only raised if @ is present.
4161        assert!(
4162            PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4163        );
4164        // @ is not allowed if the prefix is empty.
4165        assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4166    }
4167
4168    #[test]
4169    fn version_request_as_pep440_version() {
4170        // Non-concrete requests return `None`
4171        assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4172        assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4173        assert_eq!(
4174            VersionRequest::from_str(">=3.10")
4175                .unwrap()
4176                .as_pep440_version(),
4177            None
4178        );
4179
4180        // `VersionRequest::Major`
4181        assert_eq!(
4182            VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4183            Some(Version::from_str("3").unwrap())
4184        );
4185
4186        // `VersionRequest::MajorMinor`
4187        assert_eq!(
4188            VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4189            Some(Version::from_str("3.12").unwrap())
4190        );
4191
4192        // `VersionRequest::MajorMinorPatch`
4193        assert_eq!(
4194            VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4195            Some(Version::from_str("3.12.5").unwrap())
4196        );
4197
4198        // `VersionRequest::MajorMinorPrerelease`
4199        assert_eq!(
4200            VersionRequest::MajorMinorPrerelease(
4201                3,
4202                14,
4203                Prerelease {
4204                    kind: PrereleaseKind::Alpha,
4205                    number: 1
4206                },
4207                PythonVariant::Default
4208            )
4209            .as_pep440_version(),
4210            Some(Version::from_str("3.14.0a1").unwrap())
4211        );
4212        assert_eq!(
4213            VersionRequest::MajorMinorPrerelease(
4214                3,
4215                14,
4216                Prerelease {
4217                    kind: PrereleaseKind::Beta,
4218                    number: 2
4219                },
4220                PythonVariant::Default
4221            )
4222            .as_pep440_version(),
4223            Some(Version::from_str("3.14.0b2").unwrap())
4224        );
4225        assert_eq!(
4226            VersionRequest::MajorMinorPrerelease(
4227                3,
4228                13,
4229                Prerelease {
4230                    kind: PrereleaseKind::Rc,
4231                    number: 3
4232                },
4233                PythonVariant::Default
4234            )
4235            .as_pep440_version(),
4236            Some(Version::from_str("3.13.0rc3").unwrap())
4237        );
4238
4239        // Variant is ignored
4240        assert_eq!(
4241            VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4242            Some(Version::from_str("3").unwrap())
4243        );
4244        assert_eq!(
4245            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4246            Some(Version::from_str("3.13").unwrap())
4247        );
4248    }
4249
4250    #[test]
4251    fn python_request_as_pep440_version() {
4252        // `PythonRequest::Any` and `PythonRequest::Default` return `None`
4253        assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4254        assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4255
4256        // `PythonRequest::Version` delegates to `VersionRequest`
4257        assert_eq!(
4258            PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4259                .as_pep440_version(),
4260            Some(Version::from_str("3.11").unwrap())
4261        );
4262
4263        // `PythonRequest::ImplementationVersion` extracts version
4264        assert_eq!(
4265            PythonRequest::ImplementationVersion(
4266                ImplementationName::CPython,
4267                VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4268            )
4269            .as_pep440_version(),
4270            Some(Version::from_str("3.12.1").unwrap())
4271        );
4272
4273        // `PythonRequest::Implementation` returns `None` (no version)
4274        assert_eq!(
4275            PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4276            None
4277        );
4278
4279        // `PythonRequest::Key` with version
4280        assert_eq!(
4281            PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4282            Some(Version::from_str("3.13.2").unwrap())
4283        );
4284
4285        // `PythonRequest::Key` without version returns `None`
4286        assert_eq!(
4287            PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4288            None
4289        );
4290
4291        // Range versions return `None`
4292        assert_eq!(
4293            PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4294            None
4295        );
4296    }
4297}