Skip to main content

uv_python/
discovery.rs

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