Skip to main content

uv_python/
discovery.rs

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