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