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