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