Skip to main content

uv_python/
discovery.rs

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