Skip to main content

uv_python/
discovery.rs

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