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::{BrokenSymlink, 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::BrokenSymlink(BrokenSymlink { 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::BrokenSymlink(_)) => {
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::BrokenSymlink(_)) => {
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                // This is a hack to get `write_error_chain` to format things the way we want.
1563                #[derive(Debug, thiserror::Error)]
1564                #[error(
1565                    "A managed Python download is available for {0}, but an error occurred when attempting to download it."
1566                )]
1567                struct WrappedError<'a>(&'a PythonRequest, #[source] crate::Error);
1568
1569                // If the request was for the default or any version, propagate
1570                // the error as nothing else we are about to do will help the
1571                // situation.
1572                if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1573                    return Err(error);
1574                }
1575
1576                let mut error_chain = String::new();
1577                // Writing to a string can't fail with errors (panics on allocation failure)
1578                uv_warnings::write_error_chain(
1579                    &WrappedError(request, error),
1580                    &mut error_chain,
1581                    "warning",
1582                    AnsiColors::Yellow,
1583                )
1584                .unwrap();
1585                anstream::eprint!("{}", error_chain);
1586                previous_fetch_failed = true;
1587            }
1588        }
1589
1590        // If this was a request for the Default or Any version, this means that
1591        // either that's what we were called with, or we're on the last
1592        // iteration.
1593        //
1594        // The most recent find error therefore becomes a fatal one.
1595        if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1596            return Err(match error {
1597                crate::Error::MissingPython(err, _) => PythonNotFound {
1598                    // Use a more general error in this case since we looked for multiple versions
1599                    request: original_request.clone(),
1600                    python_preference: err.python_preference,
1601                    environment_preference: err.environment_preference,
1602                }
1603                .into(),
1604                other => other,
1605            });
1606        }
1607    }
1608
1609    unreachable!("The loop should have terminated when it reached PythonRequest::Default");
1610}
1611
1612/// Display a warning if the Python version of the [`Interpreter`] is unsupported by uv.
1613fn warn_on_unsupported_python(interpreter: &Interpreter) {
1614    // Warn on usage with an unsupported Python version
1615    if interpreter.python_tuple() < (3, 8) {
1616        warn_user_once!(
1617            "uv is only compatible with Python >=3.8, found Python {}",
1618            interpreter.python_version()
1619        );
1620    }
1621}
1622
1623/// On Windows we might encounter the Windows Store proxy shim (enabled in:
1624/// Settings/Apps/Advanced app settings/App execution aliases). When Python is _not_ installed
1625/// via the Windows Store, but the proxy shim is enabled, then executing `python.exe` or
1626/// `python3.exe` will redirect to the Windows Store installer.
1627///
1628/// We need to detect that these `python.exe` and `python3.exe` files are _not_ Python
1629/// executables.
1630///
1631/// This method is taken from Rye:
1632///
1633/// > This is a pretty dumb way.  We know how to parse this reparse point, but Microsoft
1634/// > does not want us to do this as the format is unstable.  So this is a best effort way.
1635/// > we just hope that the reparse point has the python redirector in it, when it's not
1636/// > pointing to a valid Python.
1637///
1638/// See: <https://github.com/astral-sh/rye/blob/b0e9eccf05fe4ff0ae7b0250a248c54f2d780b4d/rye/src/cli/shim.rs#L108>
1639#[cfg(windows)]
1640pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
1641    use std::os::windows::fs::MetadataExt;
1642    use std::os::windows::prelude::OsStrExt;
1643    use windows::Win32::Foundation::CloseHandle;
1644    use windows::Win32::Storage::FileSystem::{
1645        CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
1646        FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_MODE, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
1647        OPEN_EXISTING,
1648    };
1649    use windows::Win32::System::IO::DeviceIoControl;
1650    use windows::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
1651    use windows::core::PCWSTR;
1652
1653    // The path must be absolute.
1654    if !path.is_absolute() {
1655        return false;
1656    }
1657
1658    // The path must point to something like:
1659    //   `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python3.exe`
1660    let mut components = path.components().rev();
1661
1662    // Ex) `python.exe`, `python3.exe`, `python3.12.exe`, etc.
1663    if !components
1664        .next()
1665        .and_then(|component| component.as_os_str().to_str())
1666        .is_some_and(|component| {
1667            component.starts_with("python")
1668                && std::path::Path::new(component)
1669                    .extension()
1670                    .is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
1671        })
1672    {
1673        return false;
1674    }
1675
1676    // Ex) `WindowsApps`
1677    if components
1678        .next()
1679        .is_none_or(|component| component.as_os_str() != "WindowsApps")
1680    {
1681        return false;
1682    }
1683
1684    // Ex) `Microsoft`
1685    if components
1686        .next()
1687        .is_none_or(|component| component.as_os_str() != "Microsoft")
1688    {
1689        return false;
1690    }
1691
1692    // The file is only relevant if it's a reparse point.
1693    let Ok(md) = fs_err::symlink_metadata(path) else {
1694        return false;
1695    };
1696    if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 {
1697        return false;
1698    }
1699
1700    let mut path_encoded = path
1701        .as_os_str()
1702        .encode_wide()
1703        .chain(std::iter::once(0))
1704        .collect::<Vec<_>>();
1705
1706    // SAFETY: The path is null-terminated.
1707    #[allow(unsafe_code)]
1708    let reparse_handle = unsafe {
1709        CreateFileW(
1710            PCWSTR(path_encoded.as_mut_ptr()),
1711            0,
1712            FILE_SHARE_MODE(0),
1713            None,
1714            OPEN_EXISTING,
1715            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
1716            None,
1717        )
1718    };
1719
1720    let Ok(reparse_handle) = reparse_handle else {
1721        return false;
1722    };
1723
1724    let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
1725    let mut bytes_returned = 0;
1726
1727    // SAFETY: The buffer is large enough to hold the reparse point.
1728    #[allow(unsafe_code, clippy::cast_possible_truncation)]
1729    let success = unsafe {
1730        DeviceIoControl(
1731            reparse_handle,
1732            FSCTL_GET_REPARSE_POINT,
1733            None,
1734            0,
1735            Some(buf.as_mut_ptr().cast()),
1736            buf.len() as u32 * 2,
1737            Some(&raw mut bytes_returned),
1738            None,
1739        )
1740        .is_ok()
1741    };
1742
1743    // SAFETY: The handle is valid.
1744    #[allow(unsafe_code)]
1745    unsafe {
1746        let _ = CloseHandle(reparse_handle);
1747    }
1748
1749    // If the operation failed, assume it's not a reparse point.
1750    if !success {
1751        return false;
1752    }
1753
1754    let reparse_point = String::from_utf16_lossy(&buf[..bytes_returned as usize]);
1755    reparse_point.contains("\\AppInstallerPythonRedirector.exe")
1756}
1757
1758/// On Unix, we do not need to deal with Windows store shims.
1759///
1760/// See the Windows implementation for details.
1761#[cfg(not(windows))]
1762fn is_windows_store_shim(_path: &Path) -> bool {
1763    false
1764}
1765
1766impl PythonVariant {
1767    fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
1768        match self {
1769            Self::Default => {
1770                // TODO(zanieb): Right now, we allow debug interpreters to be selected by default for
1771                // backwards compatibility, but we may want to change this in the future.
1772                if (interpreter.python_major(), interpreter.python_minor()) >= (3, 14) {
1773                    // For Python 3.14+, the free-threaded build is not considered experimental
1774                    // and can satisfy the default variant without opt-in
1775                    true
1776                } else {
1777                    // In Python 3.13 and earlier, the free-threaded build is considered
1778                    // experimental and requires explicit opt-in
1779                    !interpreter.gil_disabled()
1780                }
1781            }
1782            Self::Debug => interpreter.debug_enabled(),
1783            Self::Freethreaded => interpreter.gil_disabled(),
1784            Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
1785            Self::Gil => !interpreter.gil_disabled(),
1786            Self::GilDebug => !interpreter.gil_disabled() && interpreter.debug_enabled(),
1787        }
1788    }
1789
1790    /// Return the executable suffix for the variant, e.g., `t` for `python3.13t`.
1791    ///
1792    /// Returns an empty string for the default Python variant.
1793    pub fn executable_suffix(self) -> &'static str {
1794        match self {
1795            Self::Default => "",
1796            Self::Debug => "d",
1797            Self::Freethreaded => "t",
1798            Self::FreethreadedDebug => "td",
1799            Self::Gil => "",
1800            Self::GilDebug => "d",
1801        }
1802    }
1803
1804    /// Return the suffix for display purposes, e.g., `+gil`.
1805    pub fn display_suffix(self) -> &'static str {
1806        match self {
1807            Self::Default => "",
1808            Self::Debug => "+debug",
1809            Self::Freethreaded => "+freethreaded",
1810            Self::FreethreadedDebug => "+freethreaded+debug",
1811            Self::Gil => "+gil",
1812            Self::GilDebug => "+gil+debug",
1813        }
1814    }
1815
1816    /// Return the lib suffix for the variant, e.g., `t` for `python3.13t` but an empty string for
1817    /// `python3.13d` or `python3.13`.
1818    pub fn lib_suffix(self) -> &'static str {
1819        match self {
1820            Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "",
1821            Self::Freethreaded | Self::FreethreadedDebug => "t",
1822        }
1823    }
1824
1825    pub fn is_freethreaded(self) -> bool {
1826        match self {
1827            Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false,
1828            Self::Freethreaded | Self::FreethreadedDebug => true,
1829        }
1830    }
1831
1832    pub fn is_debug(self) -> bool {
1833        match self {
1834            Self::Default | Self::Freethreaded | Self::Gil => false,
1835            Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true,
1836        }
1837    }
1838}
1839impl PythonRequest {
1840    /// Create a request from a string.
1841    ///
1842    /// This cannot fail, which means weird inputs will be parsed as [`PythonRequest::File`] or
1843    /// [`PythonRequest::ExecutableName`].
1844    ///
1845    /// This is intended for parsing the argument to the `--python` flag. See also
1846    /// [`try_from_tool_name`][Self::try_from_tool_name] below.
1847    pub fn parse(value: &str) -> Self {
1848        let lowercase_value = &value.to_ascii_lowercase();
1849
1850        // Literals, e.g. `any` or `default`
1851        if lowercase_value == "any" {
1852            return Self::Any;
1853        }
1854        if lowercase_value == "default" {
1855            return Self::Default;
1856        }
1857
1858        // the prefix of e.g. `python312` and the empty prefix of bare versions, e.g. `312`
1859        let abstract_version_prefixes = ["python", ""];
1860        let all_implementation_names =
1861            ImplementationName::long_names().chain(ImplementationName::short_names());
1862        // Abstract versions like `python@312`, `python312`, or `312`, plus implementations and
1863        // implementation versions like `pypy`, `pypy@312` or `pypy312`.
1864        if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1865            abstract_version_prefixes,
1866            all_implementation_names,
1867            lowercase_value,
1868        ) {
1869            return request;
1870        }
1871
1872        let value_as_path = PathBuf::from(value);
1873        // e.g. /path/to/.venv
1874        if value_as_path.is_dir() {
1875            return Self::Directory(value_as_path);
1876        }
1877        // e.g. /path/to/python
1878        if value_as_path.is_file() {
1879            return Self::File(value_as_path);
1880        }
1881
1882        // e.g. path/to/python on Windows, where path/to/python.exe is the true path
1883        #[cfg(windows)]
1884        if value_as_path.extension().is_none() {
1885            let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1886            if value_as_path.is_file() {
1887                return Self::File(value_as_path);
1888            }
1889        }
1890
1891        // During unit testing, we cannot change the working directory used by std
1892        // so we perform a check relative to the mock working directory. Ideally we'd
1893        // remove this code and use tests at the CLI level so we can change the real
1894        // directory.
1895        #[cfg(test)]
1896        if value_as_path.is_relative() {
1897            if let Ok(current_dir) = crate::current_dir() {
1898                let relative = current_dir.join(&value_as_path);
1899                if relative.is_dir() {
1900                    return Self::Directory(relative);
1901                }
1902                if relative.is_file() {
1903                    return Self::File(relative);
1904                }
1905            }
1906        }
1907        // e.g. .\path\to\python3.exe or ./path/to/python3
1908        // If it contains a path separator, we'll treat it as a full path even if it does not exist
1909        if value.contains(std::path::MAIN_SEPARATOR) {
1910            return Self::File(value_as_path);
1911        }
1912        // e.g. ./path/to/python3.exe
1913        // On Windows, Unix path separators are often valid
1914        if cfg!(windows) && value.contains('/') {
1915            return Self::File(value_as_path);
1916        }
1917        if let Ok(request) = PythonDownloadRequest::from_str(value) {
1918            return Self::Key(request);
1919        }
1920        // Finally, we'll treat it as the name of an executable (i.e. in the search PATH)
1921        // e.g. foo.exe
1922        Self::ExecutableName(value.to_string())
1923    }
1924
1925    /// Try to parse a tool name as a Python version, e.g. `uvx python311`.
1926    ///
1927    /// The `PythonRequest::parse` constructor above is intended for the `--python` flag, where the
1928    /// value is unambiguously a Python version. This alternate constructor is intended for `uvx`
1929    /// or `uvx --from`, where the executable could be either a Python version or a package name.
1930    /// There are several differences in behavior:
1931    ///
1932    /// - This only supports long names, including e.g. `pypy39` but **not** `pp39` or `39`.
1933    /// - On Windows only, this allows `pythonw` as an alias for `python`.
1934    /// - This allows `python` by itself (and on Windows, `pythonw`) as an alias for `default`.
1935    ///
1936    /// This can only return `Err` if `@` is used. Otherwise, if no match is found, it returns
1937    /// `Ok(None)`.
1938    pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
1939        let lowercase_value = &value.to_ascii_lowercase();
1940        // Omitting the empty string from these lists excludes bare versions like "39".
1941        let abstract_version_prefixes = if cfg!(windows) {
1942            &["python", "pythonw"][..]
1943        } else {
1944            &["python"][..]
1945        };
1946        // e.g. just `python`
1947        if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
1948            return Ok(Some(Self::Default));
1949        }
1950        Self::parse_versions_and_implementations(
1951            abstract_version_prefixes.iter().copied(),
1952            ImplementationName::long_names(),
1953            lowercase_value,
1954        )
1955    }
1956
1957    /// Take a value like `"python3.11"`, check whether it matches a set of abstract python
1958    /// prefixes (e.g. `"python"`, `"pythonw"`, or even `""`) or a set of specific Python
1959    /// implementations (e.g. `"cpython"` or `"pypy"`, possibly with abbreviations), and if so try
1960    /// to parse its version.
1961    ///
1962    /// This can only return `Err` if `@` is used, see
1963    /// [`try_split_prefix_and_version`][Self::try_split_prefix_and_version] below. Otherwise, if
1964    /// no match is found, it returns `Ok(None)`.
1965    fn parse_versions_and_implementations<'a>(
1966        // typically "python", possibly also "pythonw" or "" (for bare versions)
1967        abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
1968        // expected to be either long_names() or all names
1969        implementation_names: impl IntoIterator<Item = &'a str>,
1970        // the string to parse
1971        lowercase_value: &str,
1972    ) -> Result<Option<Self>, Error> {
1973        for prefix in abstract_version_prefixes {
1974            if let Some(version_request) =
1975                Self::try_split_prefix_and_version(prefix, lowercase_value)?
1976            {
1977                // e.g. `python39` or `python@39`
1978                // Note that e.g. `python` gets handled elsewhere, if at all. (It's currently
1979                // allowed in tool executables but not in --python flags.)
1980                return Ok(Some(Self::Version(version_request)));
1981            }
1982        }
1983        for implementation in implementation_names {
1984            if lowercase_value == implementation {
1985                return Ok(Some(Self::Implementation(
1986                    // e.g. `pypy`
1987                    // Safety: The name matched the possible names above
1988                    ImplementationName::from_str(implementation).unwrap(),
1989                )));
1990            }
1991            if let Some(version_request) =
1992                Self::try_split_prefix_and_version(implementation, lowercase_value)?
1993            {
1994                // e.g. `pypy39`
1995                return Ok(Some(Self::ImplementationVersion(
1996                    // Safety: The name matched the possible names above
1997                    ImplementationName::from_str(implementation).unwrap(),
1998                    version_request,
1999                )));
2000            }
2001        }
2002        Ok(None)
2003    }
2004
2005    /// Take a value like `"python3.11"`, check whether it matches a target prefix (e.g.
2006    /// `"python"`, `"pypy"`, or even `""`), and if so try to parse its version.
2007    ///
2008    /// Failing to match the prefix (e.g. `"notpython3.11"`) or failing to parse a version (e.g.
2009    /// `"python3notaversion"`) is not an error, and those cases return `Ok(None)`. The `@`
2010    /// separator is optional, and this function can only return `Err` if `@` is used. There are
2011    /// two error cases:
2012    ///
2013    /// - The value starts with `@` (e.g. `@3.11`).
2014    /// - The prefix is a match, but the version is invalid (e.g. `python@3.not.a.version`).
2015    fn try_split_prefix_and_version(
2016        prefix: &str,
2017        lowercase_value: &str,
2018    ) -> Result<Option<VersionRequest>, Error> {
2019        if lowercase_value.starts_with('@') {
2020            return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
2021        }
2022        let Some(rest) = lowercase_value.strip_prefix(prefix) else {
2023            return Ok(None);
2024        };
2025        // Just the prefix by itself (e.g. "python") is handled elsewhere.
2026        if rest.is_empty() {
2027            return Ok(None);
2028        }
2029        // The @ separator is optional. If it's present, the right half must be a version, and
2030        // parsing errors are raised to the caller.
2031        if let Some(after_at) = rest.strip_prefix('@') {
2032            if after_at == "latest" {
2033                // Handle `@latest` as a special case. It's still an error for now, but we plan to
2034                // support it. TODO(zanieb): Add `PythonRequest::Latest`
2035                return Err(Error::LatestVersionRequest);
2036            }
2037            return after_at.parse().map(Some);
2038        }
2039        // The @ was not present, so if the version fails to parse just return Ok(None). For
2040        // example, python3stuff.
2041        Ok(rest.parse().ok())
2042    }
2043
2044    /// Check if this request includes a specific patch version.
2045    pub fn includes_patch(&self) -> bool {
2046        match self {
2047            Self::Default => false,
2048            Self::Any => false,
2049            Self::Version(version_request) => version_request.patch().is_some(),
2050            Self::Directory(..) => false,
2051            Self::File(..) => false,
2052            Self::ExecutableName(..) => false,
2053            Self::Implementation(..) => false,
2054            Self::ImplementationVersion(_, version) => version.patch().is_some(),
2055            Self::Key(request) => request
2056                .version
2057                .as_ref()
2058                .is_some_and(|request| request.patch().is_some()),
2059        }
2060    }
2061
2062    /// Check if this request includes a specific prerelease version.
2063    pub fn includes_prerelease(&self) -> bool {
2064        match self {
2065            Self::Default => false,
2066            Self::Any => false,
2067            Self::Version(version_request) => version_request.prerelease().is_some(),
2068            Self::Directory(..) => false,
2069            Self::File(..) => false,
2070            Self::ExecutableName(..) => false,
2071            Self::Implementation(..) => false,
2072            Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
2073            Self::Key(request) => request
2074                .version
2075                .as_ref()
2076                .is_some_and(|request| request.prerelease().is_some()),
2077        }
2078    }
2079
2080    /// Check if a given interpreter satisfies the interpreter request.
2081    pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
2082        /// Returns `true` if the two paths refer to the same interpreter executable.
2083        fn is_same_executable(path1: &Path, path2: &Path) -> bool {
2084            path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
2085        }
2086
2087        match self {
2088            Self::Default | Self::Any => true,
2089            Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2090            Self::Directory(directory) => {
2091                // `sys.prefix` points to the environment root or `sys.executable` is the same
2092                is_same_executable(directory, interpreter.sys_prefix())
2093                    || is_same_executable(
2094                        virtualenv_python_executable(directory).as_path(),
2095                        interpreter.sys_executable(),
2096                    )
2097            }
2098            Self::File(file) => {
2099                // The interpreter satisfies the request both if it is the venv...
2100                if is_same_executable(interpreter.sys_executable(), file) {
2101                    return true;
2102                }
2103                // ...or if it is the base interpreter the venv was created from.
2104                if interpreter
2105                    .sys_base_executable()
2106                    .is_some_and(|sys_base_executable| {
2107                        is_same_executable(sys_base_executable, file)
2108                    })
2109                {
2110                    return true;
2111                }
2112                // ...or, on Windows, if both interpreters have the same base executable. On
2113                // Windows, interpreters are copied rather than symlinked, so a virtual environment
2114                // created from within a virtual environment will _not_ evaluate to the same
2115                // `sys.executable`, but will have the same `sys._base_executable`.
2116                if cfg!(windows) {
2117                    if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2118                        if let (Some(file_base), Some(interpreter_base)) = (
2119                            file_interpreter.sys_base_executable(),
2120                            interpreter.sys_base_executable(),
2121                        ) {
2122                            if is_same_executable(file_base, interpreter_base) {
2123                                return true;
2124                            }
2125                        }
2126                    }
2127                }
2128                false
2129            }
2130            Self::ExecutableName(name) => {
2131                // First, see if we have a match in the venv ...
2132                if interpreter
2133                    .sys_executable()
2134                    .file_name()
2135                    .is_some_and(|filename| filename == name.as_str())
2136                {
2137                    return true;
2138                }
2139                // ... or the venv's base interpreter (without performing IO), if that fails, ...
2140                if interpreter
2141                    .sys_base_executable()
2142                    .and_then(|executable| executable.file_name())
2143                    .is_some_and(|file_name| file_name == name.as_str())
2144                {
2145                    return true;
2146                }
2147                // ... check in `PATH`. The name we find here does not need to be the
2148                // name we install, so we can find `foopython` here which got installed as `python`.
2149                if which(name)
2150                    .ok()
2151                    .as_ref()
2152                    .and_then(|executable| executable.file_name())
2153                    .is_some_and(|file_name| file_name == name.as_str())
2154                {
2155                    return true;
2156                }
2157                false
2158            }
2159            Self::Implementation(implementation) => interpreter
2160                .implementation_name()
2161                .eq_ignore_ascii_case(implementation.into()),
2162            Self::ImplementationVersion(implementation, version) => {
2163                version.matches_interpreter(interpreter)
2164                    && interpreter
2165                        .implementation_name()
2166                        .eq_ignore_ascii_case(implementation.into())
2167            }
2168            Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2169        }
2170    }
2171
2172    /// Whether this request opts-in to a pre-release Python version.
2173    pub(crate) fn allows_prereleases(&self) -> bool {
2174        match self {
2175            Self::Default => false,
2176            Self::Any => true,
2177            Self::Version(version) => version.allows_prereleases(),
2178            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2179            Self::Implementation(_) => false,
2180            Self::ImplementationVersion(_, _) => true,
2181            Self::Key(request) => request.allows_prereleases(),
2182        }
2183    }
2184
2185    /// Whether this request opts-in to a debug Python version.
2186    pub(crate) fn allows_debug(&self) -> bool {
2187        match self {
2188            Self::Default => false,
2189            Self::Any => true,
2190            Self::Version(version) => version.is_debug(),
2191            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2192            Self::Implementation(_) => false,
2193            Self::ImplementationVersion(_, _) => true,
2194            Self::Key(request) => request.allows_debug(),
2195        }
2196    }
2197
2198    /// Whether this request opts-in to an alternative Python implementation, e.g., PyPy.
2199    pub(crate) fn allows_alternative_implementations(&self) -> bool {
2200        match self {
2201            Self::Default => false,
2202            Self::Any => true,
2203            Self::Version(_) => false,
2204            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2205            Self::Implementation(implementation)
2206            | Self::ImplementationVersion(implementation, _) => {
2207                !matches!(implementation, ImplementationName::CPython)
2208            }
2209            Self::Key(request) => request.allows_alternative_implementations(),
2210        }
2211    }
2212
2213    pub(crate) fn is_explicit_system(&self) -> bool {
2214        matches!(self, Self::File(_) | Self::Directory(_))
2215    }
2216
2217    /// Serialize the request to a canonical representation.
2218    ///
2219    /// [`Self::parse`] should always return the same request when given the output of this method.
2220    pub fn to_canonical_string(&self) -> String {
2221        match self {
2222            Self::Any => "any".to_string(),
2223            Self::Default => "default".to_string(),
2224            Self::Version(version) => version.to_string(),
2225            Self::Directory(path) => path.display().to_string(),
2226            Self::File(path) => path.display().to_string(),
2227            Self::ExecutableName(name) => name.clone(),
2228            Self::Implementation(implementation) => implementation.to_string(),
2229            Self::ImplementationVersion(implementation, version) => {
2230                format!("{implementation}@{version}")
2231            }
2232            Self::Key(request) => request.to_string(),
2233        }
2234    }
2235
2236    /// Convert an interpreter request into a concrete PEP 440 `Version` when possible.
2237    ///
2238    /// Returns `None` if the request doesn't carry an exact version
2239    pub fn as_pep440_version(&self) -> Option<Version> {
2240        match self {
2241            Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2242            Self::Key(download_request) => download_request
2243                .version()
2244                .and_then(VersionRequest::as_pep440_version),
2245            Self::Default
2246            | Self::Any
2247            | Self::Directory(_)
2248            | Self::File(_)
2249            | Self::ExecutableName(_)
2250            | Self::Implementation(_) => None,
2251        }
2252    }
2253
2254    /// Convert an interpreter request into [`VersionSpecifiers`] representing the range of
2255    /// compatible versions.
2256    ///
2257    /// Returns `None` if the request doesn't carry version constraints (e.g., a path or
2258    /// executable name).
2259    pub fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2260        match self {
2261            Self::Version(version) | Self::ImplementationVersion(_, version) => {
2262                version.as_version_specifiers()
2263            }
2264            Self::Key(download_request) => download_request
2265                .version()
2266                .and_then(VersionRequest::as_version_specifiers),
2267            Self::Default
2268            | Self::Any
2269            | Self::Directory(_)
2270            | Self::File(_)
2271            | Self::ExecutableName(_)
2272            | Self::Implementation(_) => None,
2273        }
2274    }
2275
2276    /// Returns `true` when this request is compatible with the given `requires-python` specifier.
2277    ///
2278    /// Requests without version constraints (e.g., paths, executable names) are always considered
2279    /// compatible. For versioned requests, compatibility means the request's version range has a
2280    /// non-empty intersection with the `requires-python` range.
2281    pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2282        let Some(specifiers) = self.as_version_specifiers() else {
2283            return true;
2284        };
2285
2286        let request_range = release_specifiers_to_ranges(specifiers);
2287        let requires_python_range =
2288            release_specifiers_to_ranges(requires_python.specifiers().clone());
2289        !request_range
2290            .intersection(&requires_python_range)
2291            .is_empty()
2292    }
2293}
2294
2295impl PythonSource {
2296    pub fn is_managed(self) -> bool {
2297        matches!(self, Self::Managed)
2298    }
2299
2300    /// Whether a pre-release Python installation from this source can be used without opt-in.
2301    pub(crate) fn allows_prereleases(self) -> bool {
2302        match self {
2303            Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2304            Self::SearchPath
2305            | Self::SearchPathFirst
2306            | Self::CondaPrefix
2307            | Self::BaseCondaPrefix
2308            | Self::ProvidedPath
2309            | Self::ParentInterpreter
2310            | Self::ActiveEnvironment
2311            | Self::DiscoveredEnvironment => true,
2312        }
2313    }
2314
2315    /// Whether a debug Python installation from this source can be used without opt-in.
2316    pub(crate) fn allows_debug(self) -> bool {
2317        match self {
2318            Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2319            Self::SearchPath
2320            | Self::SearchPathFirst
2321            | Self::CondaPrefix
2322            | Self::BaseCondaPrefix
2323            | Self::ProvidedPath
2324            | Self::ParentInterpreter
2325            | Self::ActiveEnvironment
2326            | Self::DiscoveredEnvironment => true,
2327        }
2328    }
2329
2330    /// Whether an alternative Python implementation from this source can be used without opt-in.
2331    pub(crate) fn allows_alternative_implementations(self) -> bool {
2332        match self {
2333            Self::Managed
2334            | Self::Registry
2335            | Self::SearchPath
2336            // TODO(zanieb): We may want to allow this at some point, but when adding this variant
2337            // we want compatibility with existing behavior
2338            | Self::SearchPathFirst
2339            | Self::MicrosoftStore => false,
2340            Self::CondaPrefix
2341            | Self::BaseCondaPrefix
2342            | Self::ProvidedPath
2343            | Self::ParentInterpreter
2344            | Self::ActiveEnvironment
2345            | Self::DiscoveredEnvironment => true,
2346        }
2347    }
2348
2349    /// Whether this source **could** be a virtual environment.
2350    ///
2351    /// This excludes the [`PythonSource::SearchPath`] although it could be in a virtual
2352    /// environment; pragmatically, that's not common and saves us from querying a bunch of system
2353    /// interpreters for no reason. It seems dubious to consider an interpreter in the `PATH` as a
2354    /// target virtual environment if it's not discovered through our virtual environment-specific
2355    /// patterns. Instead, we special case the first Python executable found on the `PATH` with
2356    /// [`PythonSource::SearchPathFirst`], allowing us to check if that's a virtual environment.
2357    /// This enables targeting the virtual environment with uv by putting its `bin/` on the `PATH`
2358    /// without setting `VIRTUAL_ENV` — but if there's another interpreter before it we will ignore
2359    /// it.
2360    pub(crate) fn is_maybe_virtualenv(self) -> bool {
2361        match self {
2362            Self::ProvidedPath
2363            | Self::ActiveEnvironment
2364            | Self::DiscoveredEnvironment
2365            | Self::CondaPrefix
2366            | Self::BaseCondaPrefix
2367            | Self::ParentInterpreter
2368            | Self::SearchPathFirst => true,
2369            Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2370        }
2371    }
2372
2373    /// Whether this source **could** be a system interpreter.
2374    pub(crate) fn is_maybe_system(self) -> bool {
2375        match self {
2376            Self::CondaPrefix
2377            | Self::BaseCondaPrefix
2378            | Self::ParentInterpreter
2379            | Self::ProvidedPath
2380            | Self::Managed
2381            | Self::SearchPath
2382            | Self::SearchPathFirst
2383            | Self::Registry
2384            | Self::MicrosoftStore => true,
2385            Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2386        }
2387    }
2388}
2389
2390impl PythonPreference {
2391    fn allows(self, source: PythonSource) -> bool {
2392        // If not dealing with a system interpreter source, we don't care about the preference
2393        if !matches!(
2394            source,
2395            PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2396        ) {
2397            return true;
2398        }
2399
2400        match self {
2401            Self::OnlyManaged => matches!(source, PythonSource::Managed),
2402            Self::Managed | Self::System => matches!(
2403                source,
2404                PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2405            ),
2406            Self::OnlySystem => {
2407                matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2408            }
2409        }
2410    }
2411
2412    pub(crate) fn allows_managed(self) -> bool {
2413        match self {
2414            Self::OnlySystem => false,
2415            Self::Managed | Self::System | Self::OnlyManaged => true,
2416        }
2417    }
2418
2419    /// Returns a new preference when the `--system` flag is used.
2420    ///
2421    /// This will convert [`PythonPreference::Managed`] to [`PythonPreference::System`] when system
2422    /// is set.
2423    #[must_use]
2424    pub fn with_system_flag(self, system: bool) -> Self {
2425        match self {
2426            // TODO(zanieb): It's not clear if we want to allow `--system` to override
2427            // `--managed-python`. We should probably make this `from_system_flag` and refactor
2428            // handling of the `PythonPreference` to use an `Option` so we can tell if the user
2429            // provided it?
2430            Self::OnlyManaged => self,
2431            Self::Managed => {
2432                if system {
2433                    Self::System
2434                } else {
2435                    self
2436                }
2437            }
2438            Self::System => self,
2439            Self::OnlySystem => self,
2440        }
2441    }
2442}
2443
2444impl PythonDownloads {
2445    pub fn is_automatic(self) -> bool {
2446        matches!(self, Self::Automatic)
2447    }
2448}
2449
2450impl EnvironmentPreference {
2451    pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2452        match (system, mutable) {
2453            // When the system flag is provided, ignore virtual environments.
2454            (true, _) => Self::OnlySystem,
2455            // For mutable operations, only allow discovery of the system with explicit selection.
2456            (false, true) => Self::ExplicitSystem,
2457            // For immutable operations, we allow discovery of the system environment
2458            (false, false) => Self::Any,
2459        }
2460    }
2461}
2462
2463#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2464pub(crate) struct ExecutableName {
2465    implementation: Option<ImplementationName>,
2466    major: Option<u8>,
2467    minor: Option<u8>,
2468    patch: Option<u8>,
2469    prerelease: Option<Prerelease>,
2470    variant: PythonVariant,
2471}
2472
2473#[derive(Debug, Clone, PartialEq, Eq)]
2474struct ExecutableNameComparator<'a> {
2475    name: ExecutableName,
2476    request: &'a VersionRequest,
2477    implementation: Option<&'a ImplementationName>,
2478}
2479
2480impl Ord for ExecutableNameComparator<'_> {
2481    /// Note the comparison returns a reverse priority ordering.
2482    ///
2483    /// Higher priority items are "Greater" than lower priority items.
2484    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2485        // Prefer the default name over a specific implementation, unless an implementation was
2486        // requested
2487        let name_ordering = if self.implementation.is_some() {
2488            std::cmp::Ordering::Greater
2489        } else {
2490            std::cmp::Ordering::Less
2491        };
2492        if self.name.implementation.is_none() && other.name.implementation.is_some() {
2493            return name_ordering.reverse();
2494        }
2495        if self.name.implementation.is_some() && other.name.implementation.is_none() {
2496            return name_ordering;
2497        }
2498        // Otherwise, use the names in supported order
2499        let ordering = self.name.implementation.cmp(&other.name.implementation);
2500        if ordering != std::cmp::Ordering::Equal {
2501            return ordering;
2502        }
2503        let ordering = self.name.major.cmp(&other.name.major);
2504        let is_default_request =
2505            matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2506        if ordering != std::cmp::Ordering::Equal {
2507            return if is_default_request {
2508                ordering.reverse()
2509            } else {
2510                ordering
2511            };
2512        }
2513        let ordering = self.name.minor.cmp(&other.name.minor);
2514        if ordering != std::cmp::Ordering::Equal {
2515            return if is_default_request {
2516                ordering.reverse()
2517            } else {
2518                ordering
2519            };
2520        }
2521        let ordering = self.name.patch.cmp(&other.name.patch);
2522        if ordering != std::cmp::Ordering::Equal {
2523            return if is_default_request {
2524                ordering.reverse()
2525            } else {
2526                ordering
2527            };
2528        }
2529        let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2530        if ordering != std::cmp::Ordering::Equal {
2531            return if is_default_request {
2532                ordering.reverse()
2533            } else {
2534                ordering
2535            };
2536        }
2537        let ordering = self.name.variant.cmp(&other.name.variant);
2538        if ordering != std::cmp::Ordering::Equal {
2539            return if is_default_request {
2540                ordering.reverse()
2541            } else {
2542                ordering
2543            };
2544        }
2545        ordering
2546    }
2547}
2548
2549impl PartialOrd for ExecutableNameComparator<'_> {
2550    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2551        Some(self.cmp(other))
2552    }
2553}
2554
2555impl ExecutableName {
2556    #[must_use]
2557    fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2558        self.implementation = Some(implementation);
2559        self
2560    }
2561
2562    #[must_use]
2563    fn with_major(mut self, major: u8) -> Self {
2564        self.major = Some(major);
2565        self
2566    }
2567
2568    #[must_use]
2569    fn with_minor(mut self, minor: u8) -> Self {
2570        self.minor = Some(minor);
2571        self
2572    }
2573
2574    #[must_use]
2575    fn with_patch(mut self, patch: u8) -> Self {
2576        self.patch = Some(patch);
2577        self
2578    }
2579
2580    #[must_use]
2581    fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2582        self.prerelease = Some(prerelease);
2583        self
2584    }
2585
2586    #[must_use]
2587    fn with_variant(mut self, variant: PythonVariant) -> Self {
2588        self.variant = variant;
2589        self
2590    }
2591
2592    fn into_comparator<'a>(
2593        self,
2594        request: &'a VersionRequest,
2595        implementation: Option<&'a ImplementationName>,
2596    ) -> ExecutableNameComparator<'a> {
2597        ExecutableNameComparator {
2598            name: self,
2599            request,
2600            implementation,
2601        }
2602    }
2603}
2604
2605impl fmt::Display for ExecutableName {
2606    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2607        if let Some(implementation) = self.implementation {
2608            write!(f, "{implementation}")?;
2609        } else {
2610            f.write_str("python")?;
2611        }
2612        if let Some(major) = self.major {
2613            write!(f, "{major}")?;
2614            if let Some(minor) = self.minor {
2615                write!(f, ".{minor}")?;
2616                if let Some(patch) = self.patch {
2617                    write!(f, ".{patch}")?;
2618                }
2619            }
2620        }
2621        if let Some(prerelease) = &self.prerelease {
2622            write!(f, "{prerelease}")?;
2623        }
2624        f.write_str(self.variant.executable_suffix())?;
2625        f.write_str(EXE_SUFFIX)?;
2626        Ok(())
2627    }
2628}
2629
2630impl VersionRequest {
2631    /// Drop any patch or prerelease information from the version request.
2632    #[must_use]
2633    pub fn only_minor(self) -> Self {
2634        match self {
2635            Self::Any => self,
2636            Self::Default => self,
2637            Self::Range(specifiers, variant) => Self::Range(
2638                specifiers
2639                    .into_iter()
2640                    .map(|s| s.only_minor_release())
2641                    .collect(),
2642                variant,
2643            ),
2644            Self::Major(..) => self,
2645            Self::MajorMinor(..) => self,
2646            Self::MajorMinorPatch(major, minor, _, variant)
2647            | Self::MajorMinorPrerelease(major, minor, _, variant) => {
2648                Self::MajorMinor(major, minor, variant)
2649            }
2650        }
2651    }
2652
2653    /// Return possible executable names for the given version request.
2654    pub(crate) fn executable_names(
2655        &self,
2656        implementation: Option<&ImplementationName>,
2657    ) -> Vec<ExecutableName> {
2658        let prerelease = if let Self::MajorMinorPrerelease(_, _, prerelease, _) = self {
2659            // Include the prerelease version, e.g., `python3.8a`
2660            Some(prerelease)
2661        } else {
2662            None
2663        };
2664
2665        // Push a default one
2666        let mut names = Vec::new();
2667        names.push(ExecutableName::default());
2668
2669        // Collect each variant depending on the number of versions
2670        if let Some(major) = self.major() {
2671            // e.g. `python3`
2672            names.push(ExecutableName::default().with_major(major));
2673            if let Some(minor) = self.minor() {
2674                // e.g., `python3.12`
2675                names.push(
2676                    ExecutableName::default()
2677                        .with_major(major)
2678                        .with_minor(minor),
2679                );
2680                if let Some(patch) = self.patch() {
2681                    // e.g, `python3.12.1`
2682                    names.push(
2683                        ExecutableName::default()
2684                            .with_major(major)
2685                            .with_minor(minor)
2686                            .with_patch(patch),
2687                    );
2688                }
2689            }
2690        } else {
2691            // Include `3` by default, e.g., `python3`
2692            names.push(ExecutableName::default().with_major(3));
2693        }
2694
2695        if let Some(prerelease) = prerelease {
2696            // Include the prerelease version, e.g., `python3.8a`
2697            for i in 0..names.len() {
2698                let name = names[i];
2699                if name.minor.is_none() {
2700                    // We don't want to include the pre-release marker here
2701                    // e.g. `pythonrc1` and `python3rc1` don't make sense
2702                    continue;
2703                }
2704                names.push(name.with_prerelease(*prerelease));
2705            }
2706        }
2707
2708        // Add all the implementation-specific names
2709        if let Some(implementation) = implementation {
2710            for i in 0..names.len() {
2711                let name = names[i].with_implementation(*implementation);
2712                names.push(name);
2713            }
2714        } else {
2715            // When looking for all implementations, include all possible names
2716            if matches!(self, Self::Any) {
2717                for i in 0..names.len() {
2718                    for implementation in ImplementationName::iter_all() {
2719                        let name = names[i].with_implementation(implementation);
2720                        names.push(name);
2721                    }
2722                }
2723            }
2724        }
2725
2726        // Include free-threaded variants
2727        if let Some(variant) = self.variant() {
2728            if variant != PythonVariant::Default {
2729                for i in 0..names.len() {
2730                    let name = names[i].with_variant(variant);
2731                    names.push(name);
2732                }
2733            }
2734        }
2735
2736        names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
2737        names.reverse();
2738
2739        names
2740    }
2741
2742    /// Return the major version segment of the request, if any.
2743    pub(crate) fn major(&self) -> Option<u8> {
2744        match self {
2745            Self::Any | Self::Default | Self::Range(_, _) => None,
2746            Self::Major(major, _) => Some(*major),
2747            Self::MajorMinor(major, _, _) => Some(*major),
2748            Self::MajorMinorPatch(major, _, _, _) => Some(*major),
2749            Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2750        }
2751    }
2752
2753    /// Return the minor version segment of the request, if any.
2754    pub(crate) fn minor(&self) -> Option<u8> {
2755        match self {
2756            Self::Any | Self::Default | Self::Range(_, _) => None,
2757            Self::Major(_, _) => None,
2758            Self::MajorMinor(_, minor, _) => Some(*minor),
2759            Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
2760            Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2761        }
2762    }
2763
2764    /// Return the patch version segment of the request, if any.
2765    pub(crate) fn patch(&self) -> Option<u8> {
2766        match self {
2767            Self::Any | Self::Default | Self::Range(_, _) => None,
2768            Self::Major(_, _) => None,
2769            Self::MajorMinor(_, _, _) => None,
2770            Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
2771            Self::MajorMinorPrerelease(_, _, _, _) => None,
2772        }
2773    }
2774
2775    /// Return the pre-release segment of the request, if any.
2776    pub(crate) fn prerelease(&self) -> Option<&Prerelease> {
2777        match self {
2778            Self::Any | Self::Default | Self::Range(_, _) => None,
2779            Self::Major(_, _) => None,
2780            Self::MajorMinor(_, _, _) => None,
2781            Self::MajorMinorPatch(_, _, _, _) => None,
2782            Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
2783        }
2784    }
2785
2786    /// Check if the request is for a version supported by uv.
2787    ///
2788    /// If not, an `Err` is returned with an explanatory message.
2789    pub(crate) fn check_supported(&self) -> Result<(), String> {
2790        match self {
2791            Self::Any | Self::Default => (),
2792            Self::Major(major, _) => {
2793                if *major < 3 {
2794                    return Err(format!(
2795                        "Python <3 is not supported but {major} was requested."
2796                    ));
2797                }
2798            }
2799            Self::MajorMinor(major, minor, _) => {
2800                if (*major, *minor) < (3, 7) {
2801                    return Err(format!(
2802                        "Python <3.7 is not supported but {major}.{minor} was requested."
2803                    ));
2804                }
2805            }
2806            Self::MajorMinorPatch(major, minor, patch, _) => {
2807                if (*major, *minor) < (3, 7) {
2808                    return Err(format!(
2809                        "Python <3.7 is not supported but {major}.{minor}.{patch} was requested."
2810                    ));
2811                }
2812            }
2813            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2814                if (*major, *minor) < (3, 7) {
2815                    return Err(format!(
2816                        "Python <3.7 is not supported but {major}.{minor}{prerelease} was requested."
2817                    ));
2818                }
2819            }
2820            // TODO(zanieb): We could do some checking here to see if the range can be satisfied
2821            Self::Range(_, _) => (),
2822        }
2823
2824        if self.is_freethreaded() {
2825            if let Self::MajorMinor(major, minor, _) = self.clone().without_patch() {
2826                if (major, minor) < (3, 13) {
2827                    return Err(format!(
2828                        "Python <3.13 does not support free-threading but {self} was requested."
2829                    ));
2830                }
2831            }
2832        }
2833
2834        Ok(())
2835    }
2836
2837    /// Change this request into a request appropriate for the given [`PythonSource`].
2838    ///
2839    /// For example, if [`VersionRequest::Default`] is requested, it will be changed to
2840    /// [`VersionRequest::Any`] for sources that should allow non-default interpreters like
2841    /// free-threaded variants.
2842    #[must_use]
2843    pub(crate) fn into_request_for_source(self, source: PythonSource) -> Self {
2844        match self {
2845            Self::Default => match source {
2846                PythonSource::ParentInterpreter
2847                | PythonSource::CondaPrefix
2848                | PythonSource::BaseCondaPrefix
2849                | PythonSource::ProvidedPath
2850                | PythonSource::DiscoveredEnvironment
2851                | PythonSource::ActiveEnvironment => Self::Any,
2852                PythonSource::SearchPath
2853                | PythonSource::SearchPathFirst
2854                | PythonSource::Registry
2855                | PythonSource::MicrosoftStore
2856                | PythonSource::Managed => Self::Default,
2857            },
2858            _ => self,
2859        }
2860    }
2861
2862    /// Check if a interpreter matches the request.
2863    pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
2864        match self {
2865            Self::Any => true,
2866            // Do not use free-threaded interpreters by default
2867            Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
2868            Self::Major(major, variant) => {
2869                interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
2870            }
2871            Self::MajorMinor(major, minor, variant) => {
2872                (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
2873                    && variant.matches_interpreter(interpreter)
2874            }
2875            Self::MajorMinorPatch(major, minor, patch, variant) => {
2876                (
2877                    interpreter.python_major(),
2878                    interpreter.python_minor(),
2879                    interpreter.python_patch(),
2880                ) == (*major, *minor, *patch)
2881                    // When a patch version is included, we treat it as a request for a stable
2882                    // release
2883                    && interpreter.python_version().pre().is_none()
2884                    && variant.matches_interpreter(interpreter)
2885            }
2886            Self::Range(specifiers, variant) => {
2887                // If the specifier contains pre-releases, use the full version for comparison.
2888                // Otherwise, strip pre-release so that, e.g., `>=3.14` matches `3.14.0rc3`.
2889                let version = if specifiers
2890                    .iter()
2891                    .any(uv_pep440::VersionSpecifier::any_prerelease)
2892                {
2893                    Cow::Borrowed(interpreter.python_version())
2894                } else {
2895                    Cow::Owned(interpreter.python_version().only_release())
2896                };
2897                specifiers.contains(&version) && variant.matches_interpreter(interpreter)
2898            }
2899            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
2900                let version = interpreter.python_version();
2901                let Some(interpreter_prerelease) = version.pre() else {
2902                    return false;
2903                };
2904                (
2905                    interpreter.python_major(),
2906                    interpreter.python_minor(),
2907                    interpreter_prerelease,
2908                ) == (*major, *minor, *prerelease)
2909                    && variant.matches_interpreter(interpreter)
2910            }
2911        }
2912    }
2913
2914    /// Check if a version is compatible with the request.
2915    ///
2916    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
2917    /// avoid querying interpreters if it's clear it cannot fulfill the request.
2918    pub(crate) fn matches_version(&self, version: &PythonVersion) -> bool {
2919        match self {
2920            Self::Any | Self::Default => true,
2921            Self::Major(major, _) => version.major() == *major,
2922            Self::MajorMinor(major, minor, _) => {
2923                (version.major(), version.minor()) == (*major, *minor)
2924            }
2925            Self::MajorMinorPatch(major, minor, patch, _) => {
2926                (version.major(), version.minor(), version.patch())
2927                    == (*major, *minor, Some(*patch))
2928            }
2929            Self::Range(specifiers, _) => {
2930                // If the specifier contains pre-releases, use the full version for comparison.
2931                // Otherwise, strip pre-release so that, e.g., `>=3.14` matches `3.14.0rc3`.
2932                let version = if specifiers
2933                    .iter()
2934                    .any(uv_pep440::VersionSpecifier::any_prerelease)
2935                {
2936                    Cow::Borrowed(&version.version)
2937                } else {
2938                    Cow::Owned(version.version.only_release())
2939                };
2940                specifiers.contains(&version)
2941            }
2942            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2943                (version.major(), version.minor(), version.pre())
2944                    == (*major, *minor, Some(*prerelease))
2945            }
2946        }
2947    }
2948
2949    /// Check if major and minor version segments are compatible with the request.
2950    ///
2951    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
2952    /// avoid querying interpreters if it's clear it cannot fulfill the request.
2953    fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
2954        match self {
2955            Self::Any | Self::Default => true,
2956            Self::Major(self_major, _) => *self_major == major,
2957            Self::MajorMinor(self_major, self_minor, _) => {
2958                (*self_major, *self_minor) == (major, minor)
2959            }
2960            Self::MajorMinorPatch(self_major, self_minor, _, _) => {
2961                (*self_major, *self_minor) == (major, minor)
2962            }
2963            Self::Range(specifiers, _) => {
2964                let range = release_specifiers_to_ranges(specifiers.clone());
2965                let Some((lower, upper)) = range.bounding_range() else {
2966                    return true;
2967                };
2968                let version = Version::new([u64::from(major), u64::from(minor)]);
2969
2970                let lower = LowerBound::new(lower.cloned());
2971                if !lower.major_minor().contains(&version) {
2972                    return false;
2973                }
2974
2975                let upper = UpperBound::new(upper.cloned());
2976                if !upper.major_minor().contains(&version) {
2977                    return false;
2978                }
2979
2980                true
2981            }
2982            Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
2983                (*self_major, *self_minor) == (major, minor)
2984            }
2985        }
2986    }
2987
2988    /// Check if major, minor, patch, and prerelease version segments are compatible with the
2989    /// request.
2990    ///
2991    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
2992    /// avoid querying interpreters if it's clear it cannot fulfill the request.
2993    pub(crate) fn matches_major_minor_patch_prerelease(
2994        &self,
2995        major: u8,
2996        minor: u8,
2997        patch: u8,
2998        prerelease: Option<Prerelease>,
2999    ) -> bool {
3000        match self {
3001            Self::Any | Self::Default => true,
3002            Self::Major(self_major, _) => *self_major == major,
3003            Self::MajorMinor(self_major, self_minor, _) => {
3004                (*self_major, *self_minor) == (major, minor)
3005            }
3006            Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
3007                (*self_major, *self_minor, *self_patch) == (major, minor, patch)
3008                    // When a patch version is included, we treat it as a request for a stable
3009                    // release
3010                    && prerelease.is_none()
3011            }
3012            Self::Range(specifiers, _) => specifiers.contains(
3013                &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
3014                    .with_pre(prerelease),
3015            ),
3016            Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3017                // Pre-releases of Python versions are always for the zero patch version
3018                (*self_major, *self_minor, 0, Some(*self_prerelease))
3019                    == (major, minor, patch, prerelease)
3020            }
3021        }
3022    }
3023
3024    /// Check if a [`PythonInstallationKey`] is compatible with the request.
3025    ///
3026    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
3027    /// avoid querying interpreters if it's clear it cannot fulfill the request.
3028    pub(crate) fn matches_installation_key(&self, key: &PythonInstallationKey) -> bool {
3029        self.matches_major_minor_patch_prerelease(key.major, key.minor, key.patch, key.prerelease())
3030    }
3031
3032    /// Whether a patch version segment is present in the request.
3033    fn has_patch(&self) -> bool {
3034        match self {
3035            Self::Any | Self::Default => false,
3036            Self::Major(..) => false,
3037            Self::MajorMinor(..) => false,
3038            Self::MajorMinorPatch(..) => true,
3039            Self::MajorMinorPrerelease(..) => false,
3040            Self::Range(_, _) => false,
3041        }
3042    }
3043
3044    /// Return a new [`VersionRequest`] without the patch version if possible.
3045    ///
3046    /// If the patch version is not present, the request is returned unchanged.
3047    #[must_use]
3048    fn without_patch(self) -> Self {
3049        match self {
3050            Self::Default => Self::Default,
3051            Self::Any => Self::Any,
3052            Self::Major(major, variant) => Self::Major(major, variant),
3053            Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3054            Self::MajorMinorPatch(major, minor, _, variant) => {
3055                Self::MajorMinor(major, minor, variant)
3056            }
3057            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3058                Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3059            }
3060            Self::Range(_, _) => self,
3061        }
3062    }
3063
3064    /// Whether this request should allow selection of pre-release versions.
3065    pub(crate) fn allows_prereleases(&self) -> bool {
3066        match self {
3067            Self::Default => false,
3068            Self::Any => true,
3069            Self::Major(..) => false,
3070            Self::MajorMinor(..) => false,
3071            Self::MajorMinorPatch(..) => false,
3072            Self::MajorMinorPrerelease(..) => true,
3073            Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3074        }
3075    }
3076
3077    /// Whether this request is for a debug Python variant.
3078    pub(crate) fn is_debug(&self) -> bool {
3079        match self {
3080            Self::Any | Self::Default => false,
3081            Self::Major(_, variant)
3082            | Self::MajorMinor(_, _, variant)
3083            | Self::MajorMinorPatch(_, _, _, variant)
3084            | Self::MajorMinorPrerelease(_, _, _, variant)
3085            | Self::Range(_, variant) => variant.is_debug(),
3086        }
3087    }
3088
3089    /// Whether this request is for a free-threaded Python variant.
3090    pub(crate) fn is_freethreaded(&self) -> bool {
3091        match self {
3092            Self::Any | Self::Default => false,
3093            Self::Major(_, variant)
3094            | Self::MajorMinor(_, _, variant)
3095            | Self::MajorMinorPatch(_, _, _, variant)
3096            | Self::MajorMinorPrerelease(_, _, _, variant)
3097            | Self::Range(_, variant) => variant.is_freethreaded(),
3098        }
3099    }
3100
3101    /// Return a new [`VersionRequest`] with the [`PythonVariant`] if it has one.
3102    ///
3103    /// This is useful for converting the string representation to pep440.
3104    #[must_use]
3105    pub fn without_python_variant(self) -> Self {
3106        // TODO(zanieb): Replace this entire function with a utility that casts this to a version
3107        // without using `VersionRequest::to_string`.
3108        match self {
3109            Self::Any | Self::Default => self,
3110            Self::Major(major, _) => Self::Major(major, PythonVariant::Default),
3111            Self::MajorMinor(major, minor, _) => {
3112                Self::MajorMinor(major, minor, PythonVariant::Default)
3113            }
3114            Self::MajorMinorPatch(major, minor, patch, _) => {
3115                Self::MajorMinorPatch(major, minor, patch, PythonVariant::Default)
3116            }
3117            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3118                Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Default)
3119            }
3120            Self::Range(specifiers, _) => Self::Range(specifiers, PythonVariant::Default),
3121        }
3122    }
3123
3124    /// Return the [`PythonVariant`] of the request, if any.
3125    pub(crate) fn variant(&self) -> Option<PythonVariant> {
3126        match self {
3127            Self::Any => None,
3128            Self::Default => Some(PythonVariant::Default),
3129            Self::Major(_, variant)
3130            | Self::MajorMinor(_, _, variant)
3131            | Self::MajorMinorPatch(_, _, _, variant)
3132            | Self::MajorMinorPrerelease(_, _, _, variant)
3133            | Self::Range(_, variant) => Some(*variant),
3134        }
3135    }
3136
3137    /// Convert this request into a concrete PEP 440 `Version` when possible.
3138    ///
3139    /// Returns `None` for non-concrete requests
3140    pub fn as_pep440_version(&self) -> Option<Version> {
3141        match self {
3142            Self::Default | Self::Any | Self::Range(_, _) => None,
3143            Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3144            Self::MajorMinor(major, minor, _) => {
3145                Some(Version::new([u64::from(*major), u64::from(*minor)]))
3146            }
3147            Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3148                u64::from(*major),
3149                u64::from(*minor),
3150                u64::from(*patch),
3151            ])),
3152            // Pre-releases of Python versions are always for the zero patch version
3153            Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3154                Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3155            ),
3156        }
3157    }
3158
3159    /// Convert this request into [`VersionSpecifiers`] representing the range of compatible
3160    /// versions.
3161    ///
3162    /// Returns `None` for requests without version constraints (e.g., [`VersionRequest::Default`]
3163    /// and [`VersionRequest::Any`]).
3164    pub fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
3165        match self {
3166            Self::Default | Self::Any => None,
3167            Self::Major(major, _) => Some(VersionSpecifiers::from(
3168                VersionSpecifier::equals_star_version(Version::new([u64::from(*major)])),
3169            )),
3170            Self::MajorMinor(major, minor, _) => Some(VersionSpecifiers::from(
3171                VersionSpecifier::equals_star_version(Version::new([
3172                    u64::from(*major),
3173                    u64::from(*minor),
3174                ])),
3175            )),
3176            Self::MajorMinorPatch(major, minor, patch, _) => {
3177                Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3178                    Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)]),
3179                )))
3180            }
3181            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3182                Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3183                    Version::new([u64::from(*major), u64::from(*minor), 0])
3184                        .with_pre(Some(*prerelease)),
3185                )))
3186            }
3187            Self::Range(specifiers, _) => Some(specifiers.clone()),
3188        }
3189    }
3190}
3191
3192impl FromStr for VersionRequest {
3193    type Err = Error;
3194
3195    fn from_str(s: &str) -> Result<Self, Self::Err> {
3196        /// Extract the variant from the end of a version request string, returning the prefix and
3197        /// the variant type.
3198        fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3199            // This cannot be a valid version, just error immediately
3200            if s.chars().all(char::is_alphabetic) {
3201                return Err(Error::InvalidVersionRequest(s.to_string()));
3202            }
3203
3204            let Some(mut start) = s.rfind(|c: char| c.is_numeric()) else {
3205                return Ok((s, PythonVariant::Default));
3206            };
3207
3208            // Advance past the first digit
3209            start += 1;
3210
3211            // Ensure we're not out of bounds
3212            if start + 1 > s.len() {
3213                return Ok((s, PythonVariant::Default));
3214            }
3215
3216            let variant = &s[start..];
3217            let prefix = &s[..start];
3218
3219            // Strip a leading `+` if present
3220            let variant = variant.strip_prefix('+').unwrap_or(variant);
3221
3222            // TODO(zanieb): Special-case error for use of `dt` instead of `td`
3223
3224            // If there's not a valid variant, fallback to failure in [`Version::from_str`]
3225            let Ok(variant) = PythonVariant::from_str(variant) else {
3226                return Ok((s, PythonVariant::Default));
3227            };
3228
3229            Ok((prefix, variant))
3230        }
3231
3232        let (s, variant) = parse_variant(s)?;
3233        let Ok(version) = Version::from_str(s) else {
3234            return parse_version_specifiers_request(s, variant);
3235        };
3236
3237        // Split the release component if it uses the wheel tag format (e.g., `38`)
3238        let version = split_wheel_tag_release_version(version);
3239
3240        // We dont allow post or dev version here
3241        if version.post().is_some() || version.dev().is_some() {
3242            return Err(Error::InvalidVersionRequest(s.to_string()));
3243        }
3244
3245        // We don't allow local version suffixes unless they're variants, in which case they'd
3246        // already be stripped.
3247        if !version.local().is_empty() {
3248            return Err(Error::InvalidVersionRequest(s.to_string()));
3249        }
3250
3251        // Cast the release components into u8s since that's what we use in `VersionRequest`
3252        let Ok(release) = try_into_u8_slice(&version.release()) else {
3253            return Err(Error::InvalidVersionRequest(s.to_string()));
3254        };
3255
3256        let prerelease = version.pre();
3257
3258        match release.as_slice() {
3259            // e.g. `3
3260            [major] => {
3261                // Prereleases are not allowed here, e.g., `3rc1` doesn't make sense
3262                if prerelease.is_some() {
3263                    return Err(Error::InvalidVersionRequest(s.to_string()));
3264                }
3265                Ok(Self::Major(*major, variant))
3266            }
3267            // e.g. `3.12` or `312` or `3.13rc1`
3268            [major, minor] => {
3269                if let Some(prerelease) = prerelease {
3270                    return Ok(Self::MajorMinorPrerelease(
3271                        *major, *minor, prerelease, variant,
3272                    ));
3273                }
3274                Ok(Self::MajorMinor(*major, *minor, variant))
3275            }
3276            // e.g. `3.12.1` or `3.13.0rc1`
3277            [major, minor, patch] => {
3278                if let Some(prerelease) = prerelease {
3279                    // Prereleases are only allowed for the first patch version, e.g, 3.12.2rc1
3280                    // isn't a proper Python release
3281                    if *patch != 0 {
3282                        return Err(Error::InvalidVersionRequest(s.to_string()));
3283                    }
3284                    return Ok(Self::MajorMinorPrerelease(
3285                        *major, *minor, prerelease, variant,
3286                    ));
3287                }
3288                Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3289            }
3290            _ => Err(Error::InvalidVersionRequest(s.to_string())),
3291        }
3292    }
3293}
3294
3295impl FromStr for PythonVariant {
3296    type Err = ();
3297
3298    fn from_str(s: &str) -> Result<Self, Self::Err> {
3299        match s {
3300            "t" | "freethreaded" => Ok(Self::Freethreaded),
3301            "d" | "debug" => Ok(Self::Debug),
3302            "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3303            "gil" => Ok(Self::Gil),
3304            "gil+debug" => Ok(Self::GilDebug),
3305            "" => Ok(Self::Default),
3306            _ => Err(()),
3307        }
3308    }
3309}
3310
3311impl fmt::Display for PythonVariant {
3312    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3313        match self {
3314            Self::Default => f.write_str("default"),
3315            Self::Debug => f.write_str("debug"),
3316            Self::Freethreaded => f.write_str("freethreaded"),
3317            Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3318            Self::Gil => f.write_str("gil"),
3319            Self::GilDebug => f.write_str("gil+debug"),
3320        }
3321    }
3322}
3323
3324fn parse_version_specifiers_request(
3325    s: &str,
3326    variant: PythonVariant,
3327) -> Result<VersionRequest, Error> {
3328    let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3329        return Err(Error::InvalidVersionRequest(s.to_string()));
3330    };
3331    if specifiers.is_empty() {
3332        return Err(Error::InvalidVersionRequest(s.to_string()));
3333    }
3334    Ok(VersionRequest::Range(specifiers, variant))
3335}
3336
3337impl From<&PythonVersion> for VersionRequest {
3338    fn from(version: &PythonVersion) -> Self {
3339        Self::from_str(&version.string)
3340            .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3341    }
3342}
3343
3344impl fmt::Display for VersionRequest {
3345    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3346        match self {
3347            Self::Any => f.write_str("any"),
3348            Self::Default => f.write_str("default"),
3349            Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3350            Self::MajorMinor(major, minor, variant) => {
3351                write!(f, "{major}.{minor}{}", variant.display_suffix())
3352            }
3353            Self::MajorMinorPatch(major, minor, patch, variant) => {
3354                write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3355            }
3356            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3357                write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3358            }
3359            Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3360        }
3361    }
3362}
3363
3364impl fmt::Display for PythonRequest {
3365    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3366        match self {
3367            Self::Default => write!(f, "a default Python"),
3368            Self::Any => write!(f, "any Python"),
3369            Self::Version(version) => write!(f, "Python {version}"),
3370            Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3371            Self::File(path) => write!(f, "path `{}`", path.user_display()),
3372            Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3373            Self::Implementation(implementation) => {
3374                write!(f, "{}", implementation.pretty())
3375            }
3376            Self::ImplementationVersion(implementation, version) => {
3377                write!(f, "{} {version}", implementation.pretty())
3378            }
3379            Self::Key(request) => write!(f, "{request}"),
3380        }
3381    }
3382}
3383
3384impl fmt::Display for PythonSource {
3385    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3386        match self {
3387            Self::ProvidedPath => f.write_str("provided path"),
3388            Self::ActiveEnvironment => f.write_str("active virtual environment"),
3389            Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3390            Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3391            Self::SearchPath => f.write_str("search path"),
3392            Self::SearchPathFirst => f.write_str("first executable in the search path"),
3393            Self::Registry => f.write_str("registry"),
3394            Self::MicrosoftStore => f.write_str("Microsoft Store"),
3395            Self::Managed => f.write_str("managed installations"),
3396            Self::ParentInterpreter => f.write_str("parent interpreter"),
3397        }
3398    }
3399}
3400
3401impl PythonPreference {
3402    /// Return the sources that are considered when searching for a Python interpreter with this
3403    /// preference.
3404    fn sources(self) -> &'static [PythonSource] {
3405        match self {
3406            Self::OnlyManaged => &[PythonSource::Managed],
3407            Self::Managed => {
3408                if cfg!(windows) {
3409                    &[
3410                        PythonSource::Managed,
3411                        PythonSource::SearchPath,
3412                        PythonSource::Registry,
3413                    ]
3414                } else {
3415                    &[PythonSource::Managed, PythonSource::SearchPath]
3416                }
3417            }
3418            Self::System => {
3419                if cfg!(windows) {
3420                    &[
3421                        PythonSource::SearchPath,
3422                        PythonSource::Registry,
3423                        PythonSource::Managed,
3424                    ]
3425                } else {
3426                    &[PythonSource::SearchPath, PythonSource::Managed]
3427                }
3428            }
3429            Self::OnlySystem => {
3430                if cfg!(windows) {
3431                    &[PythonSource::SearchPath, PythonSource::Registry]
3432                } else {
3433                    &[PythonSource::SearchPath]
3434                }
3435            }
3436        }
3437    }
3438
3439    /// Return the canonical name.
3440    // TODO(zanieb): This should be a `Display` impl and we should have a different view for
3441    // the sources
3442    pub fn canonical_name(&self) -> &'static str {
3443        match self {
3444            Self::OnlyManaged => "only managed",
3445            Self::Managed => "prefer managed",
3446            Self::System => "prefer system",
3447            Self::OnlySystem => "only system",
3448        }
3449    }
3450}
3451
3452impl fmt::Display for PythonPreference {
3453    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3454        f.write_str(match self {
3455            Self::OnlyManaged => "only managed",
3456            Self::Managed => "prefer managed",
3457            Self::System => "prefer system",
3458            Self::OnlySystem => "only system",
3459        })
3460    }
3461}
3462
3463impl DiscoveryPreferences {
3464    /// Return a string describing the sources that are considered when searching for Python with
3465    /// the given preferences.
3466    fn sources(&self, request: &PythonRequest) -> String {
3467        let python_sources = self
3468            .python_preference
3469            .sources()
3470            .iter()
3471            .map(ToString::to_string)
3472            .collect::<Vec<_>>();
3473        match self.environment_preference {
3474            EnvironmentPreference::Any => disjunction(
3475                &["virtual environments"]
3476                    .into_iter()
3477                    .chain(python_sources.iter().map(String::as_str))
3478                    .collect::<Vec<_>>(),
3479            ),
3480            EnvironmentPreference::ExplicitSystem => {
3481                if request.is_explicit_system() {
3482                    disjunction(
3483                        &["virtual environments"]
3484                            .into_iter()
3485                            .chain(python_sources.iter().map(String::as_str))
3486                            .collect::<Vec<_>>(),
3487                    )
3488                } else {
3489                    disjunction(&["virtual environments"])
3490                }
3491            }
3492            EnvironmentPreference::OnlySystem => disjunction(
3493                &python_sources
3494                    .iter()
3495                    .map(String::as_str)
3496                    .collect::<Vec<_>>(),
3497            ),
3498            EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3499        }
3500    }
3501}
3502
3503impl fmt::Display for PythonNotFound {
3504    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3505        let sources = DiscoveryPreferences {
3506            python_preference: self.python_preference,
3507            environment_preference: self.environment_preference,
3508        }
3509        .sources(&self.request);
3510
3511        match self.request {
3512            PythonRequest::Default | PythonRequest::Any => {
3513                write!(f, "No interpreter found in {sources}")
3514            }
3515            PythonRequest::File(_) => {
3516                write!(f, "No interpreter found at {}", self.request)
3517            }
3518            PythonRequest::Directory(_) => {
3519                write!(f, "No interpreter found in {}", self.request)
3520            }
3521            _ => {
3522                write!(f, "No interpreter found for {} in {sources}", self.request)
3523            }
3524        }
3525    }
3526}
3527
3528/// Join a series of items with `or` separators, making use of commas when necessary.
3529fn disjunction(items: &[&str]) -> String {
3530    match items.len() {
3531        0 => String::new(),
3532        1 => items[0].to_string(),
3533        2 => format!("{} or {}", items[0], items[1]),
3534        _ => {
3535            let last = items.last().unwrap();
3536            format!(
3537                "{}, or {}",
3538                items.iter().take(items.len() - 1).join(", "),
3539                last
3540            )
3541        }
3542    }
3543}
3544
3545fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3546    release
3547        .iter()
3548        .map(|x| match u8::try_from(*x) {
3549            Ok(x) => Ok(x),
3550            Err(e) => Err(e),
3551        })
3552        .collect()
3553}
3554
3555/// Convert a wheel tag formatted version (e.g., `38`) to multiple components (e.g., `3.8`).
3556///
3557/// The major version is always assumed to be a single digit 0-9. The minor version is all
3558/// the following content.
3559///
3560/// If not a wheel tag formatted version, the input is returned unchanged.
3561fn split_wheel_tag_release_version(version: Version) -> Version {
3562    let release = version.release();
3563    if release.len() != 1 {
3564        return version;
3565    }
3566
3567    let release = release[0].to_string();
3568    let mut chars = release.chars();
3569    let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3570        return version;
3571    };
3572
3573    let Ok(minor) = chars.as_str().parse::<u32>() else {
3574        return version;
3575    };
3576
3577    version.with_release([u64::from(major), u64::from(minor)])
3578}
3579
3580#[cfg(test)]
3581mod tests {
3582    use std::{path::PathBuf, str::FromStr};
3583
3584    use assert_fs::{TempDir, prelude::*};
3585    use target_lexicon::{Aarch64Architecture, Architecture};
3586    use test_log::test;
3587    use uv_distribution_types::RequiresPython;
3588    use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3589
3590    use crate::{
3591        discovery::{PythonRequest, VersionRequest},
3592        downloads::{ArchRequest, PythonDownloadRequest},
3593        implementation::ImplementationName,
3594    };
3595    use uv_platform::{Arch, Libc, Os};
3596
3597    use super::{
3598        DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonVariant,
3599    };
3600
3601    #[test]
3602    fn interpreter_request_from_str() {
3603        assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3604        assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
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"),
3611            PythonRequest::Version(VersionRequest::from_str(">=3.12").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        assert_eq!(
3618            PythonRequest::parse(">=3.12,<3.13"),
3619            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3620        );
3621
3622        assert_eq!(
3623            PythonRequest::parse("3.13.0a1"),
3624            PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3625        );
3626        assert_eq!(
3627            PythonRequest::parse("3.13.0b5"),
3628            PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3629        );
3630        assert_eq!(
3631            PythonRequest::parse("3.13.0rc1"),
3632            PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3633        );
3634        assert_eq!(
3635            PythonRequest::parse("3.13.1rc1"),
3636            PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3637            "Pre-release version requests require a patch version of zero"
3638        );
3639        assert_eq!(
3640            PythonRequest::parse("3rc1"),
3641            PythonRequest::ExecutableName("3rc1".to_string()),
3642            "Pre-release version requests require a minor version"
3643        );
3644
3645        assert_eq!(
3646            PythonRequest::parse("cpython"),
3647            PythonRequest::Implementation(ImplementationName::CPython)
3648        );
3649
3650        assert_eq!(
3651            PythonRequest::parse("cpython3.12.2"),
3652            PythonRequest::ImplementationVersion(
3653                ImplementationName::CPython,
3654                VersionRequest::from_str("3.12.2").unwrap(),
3655            )
3656        );
3657
3658        assert_eq!(
3659            PythonRequest::parse("cpython-3.13.2"),
3660            PythonRequest::Key(PythonDownloadRequest {
3661                version: Some(VersionRequest::MajorMinorPatch(
3662                    3,
3663                    13,
3664                    2,
3665                    PythonVariant::Default
3666                )),
3667                implementation: Some(ImplementationName::CPython),
3668                arch: None,
3669                os: None,
3670                libc: None,
3671                build: None,
3672                prereleases: None
3673            })
3674        );
3675        assert_eq!(
3676            PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3677            PythonRequest::Key(PythonDownloadRequest {
3678                version: Some(VersionRequest::MajorMinorPatch(
3679                    3,
3680                    13,
3681                    2,
3682                    PythonVariant::Default
3683                )),
3684                implementation: Some(ImplementationName::CPython),
3685                arch: Some(ArchRequest::Explicit(Arch::new(
3686                    Architecture::Aarch64(Aarch64Architecture::Aarch64),
3687                    None
3688                ))),
3689                os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3690                libc: Some(Libc::None),
3691                build: None,
3692                prereleases: None
3693            })
3694        );
3695        assert_eq!(
3696            PythonRequest::parse("any-3.13.2"),
3697            PythonRequest::Key(PythonDownloadRequest {
3698                version: Some(VersionRequest::MajorMinorPatch(
3699                    3,
3700                    13,
3701                    2,
3702                    PythonVariant::Default
3703                )),
3704                implementation: None,
3705                arch: None,
3706                os: None,
3707                libc: None,
3708                build: None,
3709                prereleases: None
3710            })
3711        );
3712        assert_eq!(
3713            PythonRequest::parse("any-3.13.2-any-aarch64"),
3714            PythonRequest::Key(PythonDownloadRequest {
3715                version: Some(VersionRequest::MajorMinorPatch(
3716                    3,
3717                    13,
3718                    2,
3719                    PythonVariant::Default
3720                )),
3721                implementation: None,
3722                arch: Some(ArchRequest::Explicit(Arch::new(
3723                    Architecture::Aarch64(Aarch64Architecture::Aarch64),
3724                    None
3725                ))),
3726                os: None,
3727                libc: None,
3728                build: None,
3729                prereleases: None
3730            })
3731        );
3732
3733        assert_eq!(
3734            PythonRequest::parse("pypy"),
3735            PythonRequest::Implementation(ImplementationName::PyPy)
3736        );
3737        assert_eq!(
3738            PythonRequest::parse("pp"),
3739            PythonRequest::Implementation(ImplementationName::PyPy)
3740        );
3741        assert_eq!(
3742            PythonRequest::parse("graalpy"),
3743            PythonRequest::Implementation(ImplementationName::GraalPy)
3744        );
3745        assert_eq!(
3746            PythonRequest::parse("gp"),
3747            PythonRequest::Implementation(ImplementationName::GraalPy)
3748        );
3749        assert_eq!(
3750            PythonRequest::parse("cp"),
3751            PythonRequest::Implementation(ImplementationName::CPython)
3752        );
3753        assert_eq!(
3754            PythonRequest::parse("pypy3.10"),
3755            PythonRequest::ImplementationVersion(
3756                ImplementationName::PyPy,
3757                VersionRequest::from_str("3.10").unwrap(),
3758            )
3759        );
3760        assert_eq!(
3761            PythonRequest::parse("pp310"),
3762            PythonRequest::ImplementationVersion(
3763                ImplementationName::PyPy,
3764                VersionRequest::from_str("3.10").unwrap(),
3765            )
3766        );
3767        assert_eq!(
3768            PythonRequest::parse("graalpy3.10"),
3769            PythonRequest::ImplementationVersion(
3770                ImplementationName::GraalPy,
3771                VersionRequest::from_str("3.10").unwrap(),
3772            )
3773        );
3774        assert_eq!(
3775            PythonRequest::parse("gp310"),
3776            PythonRequest::ImplementationVersion(
3777                ImplementationName::GraalPy,
3778                VersionRequest::from_str("3.10").unwrap(),
3779            )
3780        );
3781        assert_eq!(
3782            PythonRequest::parse("cp38"),
3783            PythonRequest::ImplementationVersion(
3784                ImplementationName::CPython,
3785                VersionRequest::from_str("3.8").unwrap(),
3786            )
3787        );
3788        assert_eq!(
3789            PythonRequest::parse("pypy@3.10"),
3790            PythonRequest::ImplementationVersion(
3791                ImplementationName::PyPy,
3792                VersionRequest::from_str("3.10").unwrap(),
3793            )
3794        );
3795        assert_eq!(
3796            PythonRequest::parse("pypy310"),
3797            PythonRequest::ImplementationVersion(
3798                ImplementationName::PyPy,
3799                VersionRequest::from_str("3.10").unwrap(),
3800            )
3801        );
3802        assert_eq!(
3803            PythonRequest::parse("graalpy@3.10"),
3804            PythonRequest::ImplementationVersion(
3805                ImplementationName::GraalPy,
3806                VersionRequest::from_str("3.10").unwrap(),
3807            )
3808        );
3809        assert_eq!(
3810            PythonRequest::parse("graalpy310"),
3811            PythonRequest::ImplementationVersion(
3812                ImplementationName::GraalPy,
3813                VersionRequest::from_str("3.10").unwrap(),
3814            )
3815        );
3816
3817        let tempdir = TempDir::new().unwrap();
3818        assert_eq!(
3819            PythonRequest::parse(tempdir.path().to_str().unwrap()),
3820            PythonRequest::Directory(tempdir.path().to_path_buf()),
3821            "An existing directory is treated as a directory"
3822        );
3823        assert_eq!(
3824            PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3825            PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3826            "A path that does not exist is treated as a file"
3827        );
3828        tempdir.child("bar").touch().unwrap();
3829        assert_eq!(
3830            PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3831            PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3832            "An existing file is treated as a file"
3833        );
3834        assert_eq!(
3835            PythonRequest::parse("./foo"),
3836            PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
3837            "A string with a file system separator is treated as a file"
3838        );
3839        assert_eq!(
3840            PythonRequest::parse("3.13t"),
3841            PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
3842        );
3843    }
3844
3845    #[test]
3846    fn discovery_sources_prefer_system_orders_search_path_first() {
3847        let preferences = DiscoveryPreferences {
3848            python_preference: PythonPreference::System,
3849            environment_preference: EnvironmentPreference::OnlySystem,
3850        };
3851        let sources = preferences.sources(&PythonRequest::Default);
3852
3853        if cfg!(windows) {
3854            assert_eq!(sources, "search path, registry, or managed installations");
3855        } else {
3856            assert_eq!(sources, "search path or managed installations");
3857        }
3858    }
3859
3860    #[test]
3861    fn discovery_sources_only_system_matches_platform_order() {
3862        let preferences = DiscoveryPreferences {
3863            python_preference: PythonPreference::OnlySystem,
3864            environment_preference: EnvironmentPreference::OnlySystem,
3865        };
3866        let sources = preferences.sources(&PythonRequest::Default);
3867
3868        if cfg!(windows) {
3869            assert_eq!(sources, "search path or registry");
3870        } else {
3871            assert_eq!(sources, "search path");
3872        }
3873    }
3874
3875    #[test]
3876    fn interpreter_request_to_canonical_string() {
3877        assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
3878        assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
3879        assert_eq!(
3880            PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
3881            "3.12"
3882        );
3883        assert_eq!(
3884            PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3885                .to_canonical_string(),
3886            ">=3.12"
3887        );
3888        assert_eq!(
3889            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3890                .to_canonical_string(),
3891            ">=3.12, <3.13"
3892        );
3893
3894        assert_eq!(
3895            PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3896                .to_canonical_string(),
3897            "3.13a1"
3898        );
3899
3900        assert_eq!(
3901            PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3902                .to_canonical_string(),
3903            "3.13b5"
3904        );
3905
3906        assert_eq!(
3907            PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3908                .to_canonical_string(),
3909            "3.13rc1"
3910        );
3911
3912        assert_eq!(
3913            PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
3914                .to_canonical_string(),
3915            "3.13rc4"
3916        );
3917
3918        assert_eq!(
3919            PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
3920            "foo"
3921        );
3922        assert_eq!(
3923            PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
3924            "cpython"
3925        );
3926        assert_eq!(
3927            PythonRequest::ImplementationVersion(
3928                ImplementationName::CPython,
3929                VersionRequest::from_str("3.12.2").unwrap(),
3930            )
3931            .to_canonical_string(),
3932            "cpython@3.12.2"
3933        );
3934        assert_eq!(
3935            PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
3936            "pypy"
3937        );
3938        assert_eq!(
3939            PythonRequest::ImplementationVersion(
3940                ImplementationName::PyPy,
3941                VersionRequest::from_str("3.10").unwrap(),
3942            )
3943            .to_canonical_string(),
3944            "pypy@3.10"
3945        );
3946        assert_eq!(
3947            PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
3948            "graalpy"
3949        );
3950        assert_eq!(
3951            PythonRequest::ImplementationVersion(
3952                ImplementationName::GraalPy,
3953                VersionRequest::from_str("3.10").unwrap(),
3954            )
3955            .to_canonical_string(),
3956            "graalpy@3.10"
3957        );
3958
3959        let tempdir = TempDir::new().unwrap();
3960        assert_eq!(
3961            PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
3962            tempdir.path().to_str().unwrap(),
3963            "An existing directory is treated as a directory"
3964        );
3965        assert_eq!(
3966            PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
3967            tempdir.child("foo").path().to_str().unwrap(),
3968            "A path that does not exist is treated as a file"
3969        );
3970        tempdir.child("bar").touch().unwrap();
3971        assert_eq!(
3972            PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
3973            tempdir.child("bar").path().to_str().unwrap(),
3974            "An existing file is treated as a file"
3975        );
3976        assert_eq!(
3977            PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
3978            "./foo",
3979            "A string with a file system separator is treated as a file"
3980        );
3981    }
3982
3983    #[test]
3984    fn version_request_from_str() {
3985        assert_eq!(
3986            VersionRequest::from_str("3").unwrap(),
3987            VersionRequest::Major(3, PythonVariant::Default)
3988        );
3989        assert_eq!(
3990            VersionRequest::from_str("3.12").unwrap(),
3991            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
3992        );
3993        assert_eq!(
3994            VersionRequest::from_str("3.12.1").unwrap(),
3995            VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
3996        );
3997        assert!(VersionRequest::from_str("1.foo.1").is_err());
3998        assert_eq!(
3999            VersionRequest::from_str("3").unwrap(),
4000            VersionRequest::Major(3, PythonVariant::Default)
4001        );
4002        assert_eq!(
4003            VersionRequest::from_str("38").unwrap(),
4004            VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
4005        );
4006        assert_eq!(
4007            VersionRequest::from_str("312").unwrap(),
4008            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4009        );
4010        assert_eq!(
4011            VersionRequest::from_str("3100").unwrap(),
4012            VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
4013        );
4014        assert_eq!(
4015            VersionRequest::from_str("3.13a1").unwrap(),
4016            VersionRequest::MajorMinorPrerelease(
4017                3,
4018                13,
4019                Prerelease {
4020                    kind: PrereleaseKind::Alpha,
4021                    number: 1
4022                },
4023                PythonVariant::Default
4024            )
4025        );
4026        assert_eq!(
4027            VersionRequest::from_str("313b1").unwrap(),
4028            VersionRequest::MajorMinorPrerelease(
4029                3,
4030                13,
4031                Prerelease {
4032                    kind: PrereleaseKind::Beta,
4033                    number: 1
4034                },
4035                PythonVariant::Default
4036            )
4037        );
4038        assert_eq!(
4039            VersionRequest::from_str("3.13.0b2").unwrap(),
4040            VersionRequest::MajorMinorPrerelease(
4041                3,
4042                13,
4043                Prerelease {
4044                    kind: PrereleaseKind::Beta,
4045                    number: 2
4046                },
4047                PythonVariant::Default
4048            )
4049        );
4050        assert_eq!(
4051            VersionRequest::from_str("3.13.0rc3").unwrap(),
4052            VersionRequest::MajorMinorPrerelease(
4053                3,
4054                13,
4055                Prerelease {
4056                    kind: PrereleaseKind::Rc,
4057                    number: 3
4058                },
4059                PythonVariant::Default
4060            )
4061        );
4062        assert!(
4063            matches!(
4064                VersionRequest::from_str("3rc1"),
4065                Err(Error::InvalidVersionRequest(_))
4066            ),
4067            "Pre-release version requests require a minor version"
4068        );
4069        assert!(
4070            matches!(
4071                VersionRequest::from_str("3.13.2rc1"),
4072                Err(Error::InvalidVersionRequest(_))
4073            ),
4074            "Pre-release version requests require a patch version of zero"
4075        );
4076        assert!(
4077            matches!(
4078                VersionRequest::from_str("3.12-dev"),
4079                Err(Error::InvalidVersionRequest(_))
4080            ),
4081            "Development version segments are not allowed"
4082        );
4083        assert!(
4084            matches!(
4085                VersionRequest::from_str("3.12+local"),
4086                Err(Error::InvalidVersionRequest(_))
4087            ),
4088            "Local version segments are not allowed"
4089        );
4090        assert!(
4091            matches!(
4092                VersionRequest::from_str("3.12.post0"),
4093                Err(Error::InvalidVersionRequest(_))
4094            ),
4095            "Post version segments are not allowed"
4096        );
4097        assert!(
4098            // Test for overflow
4099            matches!(
4100                VersionRequest::from_str("31000"),
4101                Err(Error::InvalidVersionRequest(_))
4102            )
4103        );
4104        assert_eq!(
4105            VersionRequest::from_str("3t").unwrap(),
4106            VersionRequest::Major(3, PythonVariant::Freethreaded)
4107        );
4108        assert_eq!(
4109            VersionRequest::from_str("313t").unwrap(),
4110            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4111        );
4112        assert_eq!(
4113            VersionRequest::from_str("3.13t").unwrap(),
4114            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4115        );
4116        assert_eq!(
4117            VersionRequest::from_str(">=3.13t").unwrap(),
4118            VersionRequest::Range(
4119                VersionSpecifiers::from_str(">=3.13").unwrap(),
4120                PythonVariant::Freethreaded
4121            )
4122        );
4123        assert_eq!(
4124            VersionRequest::from_str(">=3.13").unwrap(),
4125            VersionRequest::Range(
4126                VersionSpecifiers::from_str(">=3.13").unwrap(),
4127                PythonVariant::Default
4128            )
4129        );
4130        assert_eq!(
4131            VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4132            VersionRequest::Range(
4133                VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4134                PythonVariant::Freethreaded
4135            )
4136        );
4137        assert!(matches!(
4138            VersionRequest::from_str("3.13tt"),
4139            Err(Error::InvalidVersionRequest(_))
4140        ));
4141    }
4142
4143    #[test]
4144    fn executable_names_from_request() {
4145        fn case(request: &str, expected: &[&str]) {
4146            let (implementation, version) = match PythonRequest::parse(request) {
4147                PythonRequest::Any => (None, VersionRequest::Any),
4148                PythonRequest::Default => (None, VersionRequest::Default),
4149                PythonRequest::Version(version) => (None, version),
4150                PythonRequest::ImplementationVersion(implementation, version) => {
4151                    (Some(implementation), version)
4152                }
4153                PythonRequest::Implementation(implementation) => {
4154                    (Some(implementation), VersionRequest::Default)
4155                }
4156                result => {
4157                    panic!("Test cases should request versions or implementations; got {result:?}")
4158                }
4159            };
4160
4161            let result: Vec<_> = version
4162                .executable_names(implementation.as_ref())
4163                .into_iter()
4164                .map(|name| name.to_string())
4165                .collect();
4166
4167            let expected: Vec<_> = expected
4168                .iter()
4169                .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4170                .collect();
4171
4172            assert_eq!(result, expected, "mismatch for case \"{request}\"");
4173        }
4174
4175        case(
4176            "any",
4177            &[
4178                "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4179                "pyodide", "pyodide3",
4180            ],
4181        );
4182
4183        case("default", &["python", "python3"]);
4184
4185        case("3", &["python3", "python"]);
4186
4187        case("4", &["python4", "python"]);
4188
4189        case("3.13", &["python3.13", "python3", "python"]);
4190
4191        case("pypy", &["pypy", "pypy3", "python", "python3"]);
4192
4193        case(
4194            "pypy@3.10",
4195            &[
4196                "pypy3.10",
4197                "pypy3",
4198                "pypy",
4199                "python3.10",
4200                "python3",
4201                "python",
4202            ],
4203        );
4204
4205        case(
4206            "3.13t",
4207            &[
4208                "python3.13t",
4209                "python3.13",
4210                "python3t",
4211                "python3",
4212                "pythont",
4213                "python",
4214            ],
4215        );
4216        case("3t", &["python3t", "python3", "pythont", "python"]);
4217
4218        case(
4219            "3.13.2",
4220            &["python3.13.2", "python3.13", "python3", "python"],
4221        );
4222
4223        case(
4224            "3.13rc2",
4225            &["python3.13rc2", "python3.13", "python3", "python"],
4226        );
4227    }
4228
4229    #[test]
4230    fn test_try_split_prefix_and_version() {
4231        assert!(matches!(
4232            PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4233            Ok(None),
4234        ));
4235        assert!(matches!(
4236            PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4237            Ok(Some(_)),
4238        ));
4239        assert!(matches!(
4240            PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4241            Ok(Some(_)),
4242        ));
4243        assert!(matches!(
4244            PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4245            Ok(None),
4246        ));
4247        // Version parsing errors are only raised if @ is present.
4248        assert!(
4249            PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4250        );
4251        // @ is not allowed if the prefix is empty.
4252        assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4253    }
4254
4255    #[test]
4256    fn version_request_as_pep440_version() {
4257        // Non-concrete requests return `None`
4258        assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4259        assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4260        assert_eq!(
4261            VersionRequest::from_str(">=3.10")
4262                .unwrap()
4263                .as_pep440_version(),
4264            None
4265        );
4266
4267        // `VersionRequest::Major`
4268        assert_eq!(
4269            VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4270            Some(Version::from_str("3").unwrap())
4271        );
4272
4273        // `VersionRequest::MajorMinor`
4274        assert_eq!(
4275            VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4276            Some(Version::from_str("3.12").unwrap())
4277        );
4278
4279        // `VersionRequest::MajorMinorPatch`
4280        assert_eq!(
4281            VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4282            Some(Version::from_str("3.12.5").unwrap())
4283        );
4284
4285        // `VersionRequest::MajorMinorPrerelease`
4286        assert_eq!(
4287            VersionRequest::MajorMinorPrerelease(
4288                3,
4289                14,
4290                Prerelease {
4291                    kind: PrereleaseKind::Alpha,
4292                    number: 1
4293                },
4294                PythonVariant::Default
4295            )
4296            .as_pep440_version(),
4297            Some(Version::from_str("3.14.0a1").unwrap())
4298        );
4299        assert_eq!(
4300            VersionRequest::MajorMinorPrerelease(
4301                3,
4302                14,
4303                Prerelease {
4304                    kind: PrereleaseKind::Beta,
4305                    number: 2
4306                },
4307                PythonVariant::Default
4308            )
4309            .as_pep440_version(),
4310            Some(Version::from_str("3.14.0b2").unwrap())
4311        );
4312        assert_eq!(
4313            VersionRequest::MajorMinorPrerelease(
4314                3,
4315                13,
4316                Prerelease {
4317                    kind: PrereleaseKind::Rc,
4318                    number: 3
4319                },
4320                PythonVariant::Default
4321            )
4322            .as_pep440_version(),
4323            Some(Version::from_str("3.13.0rc3").unwrap())
4324        );
4325
4326        // Variant is ignored
4327        assert_eq!(
4328            VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4329            Some(Version::from_str("3").unwrap())
4330        );
4331        assert_eq!(
4332            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4333            Some(Version::from_str("3.13").unwrap())
4334        );
4335    }
4336
4337    #[test]
4338    fn python_request_as_pep440_version() {
4339        // `PythonRequest::Any` and `PythonRequest::Default` return `None`
4340        assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4341        assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4342
4343        // `PythonRequest::Version` delegates to `VersionRequest`
4344        assert_eq!(
4345            PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4346                .as_pep440_version(),
4347            Some(Version::from_str("3.11").unwrap())
4348        );
4349
4350        // `PythonRequest::ImplementationVersion` extracts version
4351        assert_eq!(
4352            PythonRequest::ImplementationVersion(
4353                ImplementationName::CPython,
4354                VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4355            )
4356            .as_pep440_version(),
4357            Some(Version::from_str("3.12.1").unwrap())
4358        );
4359
4360        // `PythonRequest::Implementation` returns `None` (no version)
4361        assert_eq!(
4362            PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4363            None
4364        );
4365
4366        // `PythonRequest::Key` with version
4367        assert_eq!(
4368            PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4369            Some(Version::from_str("3.13.2").unwrap())
4370        );
4371
4372        // `PythonRequest::Key` without version returns `None`
4373        assert_eq!(
4374            PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4375            None
4376        );
4377
4378        // Range versions return `None`
4379        assert_eq!(
4380            PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4381            None
4382        );
4383    }
4384
4385    #[test]
4386    fn intersects_requires_python_exact() {
4387        let requires_python =
4388            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4389
4390        assert!(PythonRequest::parse("3.12").intersects_requires_python(&requires_python));
4391        assert!(!PythonRequest::parse("3.11").intersects_requires_python(&requires_python));
4392    }
4393
4394    #[test]
4395    fn intersects_requires_python_major() {
4396        let requires_python =
4397            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4398
4399        // `3` overlaps with `>=3.12` (e.g., 3.12, 3.13, ... are all Python 3)
4400        assert!(PythonRequest::parse("3").intersects_requires_python(&requires_python));
4401        // `2` does not overlap with `>=3.12`
4402        assert!(!PythonRequest::parse("2").intersects_requires_python(&requires_python));
4403    }
4404
4405    #[test]
4406    fn intersects_requires_python_range() {
4407        let requires_python =
4408            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4409
4410        assert!(PythonRequest::parse(">=3.12,<3.13").intersects_requires_python(&requires_python));
4411        assert!(!PythonRequest::parse(">=3.10,<3.12").intersects_requires_python(&requires_python));
4412    }
4413
4414    #[test]
4415    fn intersects_requires_python_implementation_range() {
4416        let requires_python =
4417            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4418
4419        assert!(
4420            PythonRequest::parse("cpython@>=3.12,<3.13")
4421                .intersects_requires_python(&requires_python)
4422        );
4423        assert!(
4424            !PythonRequest::parse("cpython@>=3.10,<3.12")
4425                .intersects_requires_python(&requires_python)
4426        );
4427    }
4428
4429    #[test]
4430    fn intersects_requires_python_no_version() {
4431        let requires_python =
4432            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4433
4434        // Requests without version constraints are always compatible
4435        assert!(PythonRequest::Any.intersects_requires_python(&requires_python));
4436        assert!(PythonRequest::Default.intersects_requires_python(&requires_python));
4437        assert!(
4438            PythonRequest::Implementation(ImplementationName::CPython)
4439                .intersects_requires_python(&requires_python)
4440        );
4441    }
4442}