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_fs::Simplified;
17use uv_fs::which::is_executable;
18use uv_pep440::{
19    LowerBound, Prerelease, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
20    release_specifiers_to_ranges,
21};
22use uv_static::EnvVars;
23use uv_warnings::{warn_user_once, write_warning_chain};
24use which::{which, which_all};
25
26use crate::downloads::{ManagedPythonDownloadList, PlatformRequest, PythonDownloadRequest};
27use crate::implementation::ImplementationName;
28use crate::installation::{PythonInstallation, PythonInstallationKey};
29use crate::interpreter::Error as InterpreterError;
30use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
31use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
32#[cfg(windows)]
33use crate::microsoft_store::find_microsoft_store_pythons;
34use crate::python_version::python_build_versions_from_env;
35use crate::virtualenv::Error as VirtualEnvError;
36use crate::virtualenv::{
37    CondaEnvironmentKind, conda_environment_from_env, virtualenv_from_env,
38    virtualenv_from_working_dir, virtualenv_python_executable,
39};
40#[cfg(windows)]
41use crate::windows_registry::{WindowsPython, registry_pythons};
42use crate::{BrokenLink, Interpreter, PythonVersion};
43
44/// A request to find a Python installation.
45///
46/// See [`PythonRequest::from_str`].
47#[derive(Debug, Clone, Eq, Default)]
48pub enum PythonRequest {
49    /// An appropriate default Python installation
50    ///
51    /// This may skip some Python installations, such as pre-release versions or alternative
52    /// implementations.
53    #[default]
54    Default,
55    /// Any Python installation
56    Any,
57    /// A Python version without an implementation name e.g. `3.10` or `>=3.12,<3.13`
58    Version(VersionRequest),
59    /// A path to a directory containing a Python installation, e.g. `.venv`
60    Directory(PathBuf),
61    /// A path to a Python executable e.g. `~/bin/python`
62    File(PathBuf),
63    /// The name of a Python executable (i.e. for lookup in the PATH) e.g. `foopython3`
64    ExecutableName(String),
65    /// A Python implementation without a version e.g. `pypy` or `pp`
66    Implementation(ImplementationName),
67    /// A Python implementation name and version e.g. `pypy3.8` or `pypy@3.8` or `pp38`
68    ImplementationVersion(ImplementationName, VersionRequest),
69    /// A request for a specific Python installation key e.g. `cpython-3.12-x86_64-linux-gnu`
70    /// Generally these refer to managed Python downloads.
71    Key(PythonDownloadRequest),
72}
73
74impl PartialEq for PythonRequest {
75    fn eq(&self, other: &Self) -> bool {
76        self.to_canonical_string() == other.to_canonical_string()
77    }
78}
79
80impl std::hash::Hash for PythonRequest {
81    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
82        self.to_canonical_string().hash(state);
83    }
84}
85
86impl<'a> serde::Deserialize<'a> for PythonRequest {
87    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
88    where
89        D: serde::Deserializer<'a>,
90    {
91        let s = <Cow<'_, str>>::deserialize(deserializer)?;
92        Ok(Self::parse(&s))
93    }
94}
95
96impl serde::Serialize for PythonRequest {
97    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
98    where
99        S: serde::Serializer,
100    {
101        let s = self.to_canonical_string();
102        serializer.serialize_str(&s)
103    }
104}
105
106#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
107#[serde(deny_unknown_fields, rename_all = "kebab-case")]
108#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
109#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
110pub enum PythonPreference {
111    /// Only use managed Python installations; never use system Python installations.
112    OnlyManaged,
113    #[default]
114    /// Prefer managed Python installations over system Python installations.
115    ///
116    /// System Python installations are still preferred over downloading managed Python versions.
117    /// Use `only-managed` to always fetch a managed Python version.
118    Managed,
119    /// Prefer system Python installations over managed Python installations.
120    ///
121    /// If a system Python installation cannot be found, a managed Python installation can be used.
122    System,
123    /// Only use system Python installations; never use managed Python installations.
124    OnlySystem,
125}
126
127#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
128#[serde(deny_unknown_fields, rename_all = "kebab-case")]
129#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
130#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
131pub enum PythonDownloads {
132    /// Automatically download managed Python installations when needed.
133    #[default]
134    #[serde(alias = "auto")]
135    Automatic,
136    /// Do not automatically download managed Python installations; require explicit installation.
137    Manual,
138    /// Do not ever allow Python downloads.
139    Never,
140}
141
142impl FromStr for PythonDownloads {
143    type Err = String;
144
145    fn from_str(s: &str) -> Result<Self, Self::Err> {
146        match s.to_ascii_lowercase().as_str() {
147            "auto" | "automatic" | "true" | "1" => Ok(Self::Automatic),
148            "manual" => Ok(Self::Manual),
149            "never" | "false" | "0" => Ok(Self::Never),
150            _ => Err(format!("Invalid value for `python-download`: '{s}'")),
151        }
152    }
153}
154
155impl From<bool> for PythonDownloads {
156    fn from(value: bool) -> Self {
157        if value { Self::Automatic } else { Self::Never }
158    }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
162pub enum EnvironmentPreference {
163    /// Only use virtual environments, never allow a system environment.
164    #[default]
165    OnlyVirtual,
166    /// Prefer virtual environments and allow a system environment if explicitly requested.
167    ExplicitSystem,
168    /// Only use a system environment, ignore virtual environments.
169    OnlySystem,
170    /// Allow any environment.
171    Any,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Default)]
175pub(crate) struct DiscoveryPreferences {
176    python_preference: PythonPreference,
177    environment_preference: EnvironmentPreference,
178}
179
180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
181pub enum PythonVariant {
182    #[default]
183    Default,
184    Debug,
185    Freethreaded,
186    FreethreadedDebug,
187    Gil,
188    GilDebug,
189}
190
191/// A Python discovery version request.
192#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
193pub enum VersionRequest {
194    /// Allow an appropriate default Python version.
195    #[default]
196    Default,
197    /// Allow any Python version.
198    Any,
199    Major(u8, PythonVariant),
200    MajorMinor(u8, u8, PythonVariant),
201    MajorMinorPatch(u8, u8, u8, PythonVariant),
202    MajorMinorPrerelease(u8, u8, Prerelease, PythonVariant),
203    MajorMinorPatchPrerelease(u8, u8, u8, Prerelease, PythonVariant),
204    Range(VersionSpecifiers, PythonVariant),
205}
206
207/// The result of an Python installation search.
208///
209/// Returned by [`find_python_installation`].
210type FindPythonResult = Result<PythonInstallation, PythonNotFound>;
211
212/// The result of failed Python installation discovery.
213///
214/// See [`FindPythonResult`].
215#[derive(Clone, Debug, Error)]
216pub struct PythonNotFound {
217    pub(crate) request: PythonRequest,
218    pub(crate) python_preference: PythonPreference,
219    pub(crate) environment_preference: EnvironmentPreference,
220}
221
222/// A location for discovery of a Python installation or interpreter.
223#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)]
224pub enum PythonSource {
225    /// The path was provided directly
226    ProvidedPath,
227    /// An environment was active e.g. via `VIRTUAL_ENV`
228    ActiveEnvironment,
229    /// A conda environment was active e.g. via `CONDA_PREFIX`
230    CondaPrefix,
231    /// A base conda environment was active e.g. via `CONDA_PREFIX`
232    BaseCondaPrefix,
233    /// An environment was discovered e.g. via `.venv`
234    DiscoveredEnvironment,
235    /// An executable was found in the search path i.e. `PATH`
236    SearchPath,
237    /// The first executable found in the search path i.e. `PATH`
238    SearchPathFirst,
239    /// An executable was found in the Windows registry via PEP 514
240    Registry,
241    /// An executable was found in the known Microsoft Store locations
242    MicrosoftStore,
243    /// The Python installation was found in the uv managed Python directory
244    Managed,
245    /// The Python installation was found via the invoking interpreter i.e. via `python -m uv ...`
246    ParentInterpreter,
247}
248
249#[derive(Error, Debug)]
250pub enum Error {
251    #[error(transparent)]
252    Io(#[from] io::Error),
253
254    /// An error was encountering when retrieving interpreter information.
255    #[error("Failed to inspect Python interpreter from {} at `{}` ", _2, _1.user_display())]
256    Query(
257        #[source] Box<crate::interpreter::Error>,
258        PathBuf,
259        PythonSource,
260    ),
261
262    /// An error was encountered while trying to find a managed Python installation matching the
263    /// current platform.
264    #[error("Failed to discover managed Python installations")]
265    ManagedPython(#[from] crate::managed::Error),
266
267    /// An error was encountered when inspecting a virtual environment.
268    #[error(transparent)]
269    VirtualEnv(#[from] crate::virtualenv::Error),
270
271    #[cfg(windows)]
272    #[error("Failed to query installed Python versions from the Windows registry")]
273    RegistryError(#[from] windows::core::Error),
274
275    #[error(transparent)]
276    InvalidEnvironmentVariable(#[from] uv_static::InvalidEnvironmentVariable),
277
278    /// An invalid version request was given
279    #[error("Invalid version request: {0}")]
280    InvalidVersionRequest(String),
281
282    /// The @latest version request was given
283    #[error("Requesting the 'latest' Python version is not yet supported")]
284    LatestVersionRequest,
285
286    // TODO(zanieb): Is this error case necessary still? We should probably drop it.
287    #[error("Interpreter discovery for `{0}` requires `{1}` but only `{2}` is allowed")]
288    SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
289
290    #[error(transparent)]
291    BuildVersion(#[from] crate::python_version::BuildVersionError),
292}
293
294impl uv_errors::Hint for Error {
295    fn hints(&self) -> uv_errors::Hints<'_> {
296        match self {
297            Self::Query(err, _, _) => err.hints(),
298            _ => uv_errors::Hints::none(),
299        }
300    }
301}
302
303/// Lazily iterate over Python executables in mutable virtual environments.
304///
305/// The following sources are supported:
306///
307/// - Active virtual environment (via `VIRTUAL_ENV`)
308/// - Discovered virtual environment (e.g. `.venv` in a parent directory)
309///
310/// Notably, "system" environments are excluded. See [`python_executables_from_installed`].
311fn python_executables_from_virtual_environments<'a>()
312-> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a {
313    let from_active_environment = iter::once_with(|| {
314        virtualenv_from_env()
315            .into_iter()
316            .map(virtualenv_python_executable)
317            .map(|path| Ok((PythonSource::ActiveEnvironment, path)))
318    })
319    .flatten();
320
321    // N.B. we prefer the conda environment over discovered virtual environments
322    let from_conda_environment = iter::once_with(move || {
323        conda_environment_from_env(CondaEnvironmentKind::Child)
324            .into_iter()
325            .map(virtualenv_python_executable)
326            .map(|path| Ok((PythonSource::CondaPrefix, path)))
327    })
328    .flatten();
329
330    let from_discovered_environment = iter::once_with(|| {
331        virtualenv_from_working_dir()
332            .map(|path| {
333                path.map(virtualenv_python_executable)
334                    .map(|path| (PythonSource::DiscoveredEnvironment, path))
335                    .into_iter()
336            })
337            .map_err(Error::from)
338    })
339    .flatten_ok();
340
341    from_active_environment
342        .chain(from_conda_environment)
343        .chain(from_discovered_environment)
344}
345
346/// Lazily iterate over Python executables installed on the system.
347///
348/// The following sources are supported:
349///
350/// - Managed Python installations (e.g. `uv python install`)
351/// - The search path (i.e. `PATH`)
352/// - The registry (Windows only)
353///
354/// The ordering and presence of each source is determined by the [`PythonPreference`].
355///
356/// If a [`VersionRequest`] is provided, we will skip executables that we know do not satisfy the request
357/// and (as discussed in [`python_executables_from_search_path`]) additional version-specific executables may
358/// be included. However, the caller MUST query the returned executables to ensure they satisfy the request;
359/// this function does not guarantee that the executables provide any particular version. See
360/// [`find_python_installation`] instead.
361///
362/// This function does not guarantee that the executables are valid Python interpreters.
363/// See [`python_interpreters_from_executables`].
364fn python_executables_from_installed<'a>(
365    version: &'a VersionRequest,
366    implementation: Option<&'a ImplementationName>,
367    platform: PlatformRequest,
368    preference: PythonPreference,
369) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
370    let from_managed_installations = iter::once_with(move || {
371        ManagedPythonInstallations::from_settings(None)
372            .map_err(Error::from)
373            .and_then(|installed_installations| {
374                debug!(
375                    "Searching for managed installations at `{}`",
376                    installed_installations.root().user_display()
377                );
378                let installations = ManagedPythonInstallations::find_matching_current_platform()?;
379
380                let build_versions = python_build_versions_from_env()?;
381
382                // Check that the Python version and platform satisfy the request to avoid
383                // unnecessary interpreter queries later
384                Ok(installations
385                    .into_iter()
386                    .filter(move |installation| {
387                        if !version.matches_version(&installation.version()) {
388                            debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`");
389                            return false;
390                        }
391                        if !platform.matches(installation.platform()) {
392                            debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
393                            return false;
394                        }
395
396                        if let Some(requested_build) = build_versions.get(&installation.implementation()) {
397                            let Some(installation_build) = installation.build() else {
398                                debug!(
399                                    "Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation"
400                                );
401                                return false;
402                            };
403                            if installation_build != requested_build {
404                                debug!(
405                                    "Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`"
406                                );
407                                return false;
408                            }
409                        }
410
411                        true
412                    })
413                    .inspect(|installation| debug!("Found managed installation `{installation}`"))
414                    .map(move |installation| {
415                        // If it's not a patch version request, then attempt to read the stable
416                        // minor version link.
417                        let executable = version
418                                .patch()
419                                .is_none()
420                                .then(|| {
421                                    PythonMinorVersionLink::from_installation(
422                                        &installation,
423                                    )
424                                    .filter(PythonMinorVersionLink::exists)
425                                    .map(
426                                        |minor_version_link| {
427                                            minor_version_link.symlink_executable.clone()
428                                        },
429                                    )
430                                })
431                                .flatten()
432                                .unwrap_or_else(|| installation.executable(false));
433                        (PythonSource::Managed, executable)
434                    })
435                )
436            })
437    })
438    .flatten_ok();
439
440    let from_search_path = iter::once_with(move || {
441        python_executables_from_search_path(version, implementation)
442            .enumerate()
443            .map(|(i, path)| {
444                if i == 0 {
445                    Ok((PythonSource::SearchPathFirst, path))
446                } else {
447                    Ok((PythonSource::SearchPath, path))
448                }
449            })
450    })
451    .flatten();
452
453    #[cfg(windows)]
454    let from_windows_registry: Box<
455        dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
456    > = match uv_static::parse_boolish_environment_variable(EnvVars::UV_PYTHON_NO_REGISTRY) {
457        Ok(Some(true)) => Box::new(iter::empty()),
458        Ok(Some(false) | None) => Box::new(
459            iter::once_with(move || {
460                // Skip interpreter probing if we already know the version doesn't match.
461                let version_filter = move |entry: &WindowsPython| {
462                    if let Some(found) = &entry.version {
463                        // Some distributions emit the patch version (example: `SysVersion: 3.9`)
464                        if found.string.chars().filter(|c| *c == '.').count() == 1 {
465                            version.matches_major_minor(found.major(), found.minor())
466                        } else {
467                            version.matches_version(found)
468                        }
469                    } else {
470                        true
471                    }
472                };
473
474                registry_pythons()
475                    .map(|entries| {
476                        entries
477                            .into_iter()
478                            .filter(version_filter)
479                            .map(|entry| (PythonSource::Registry, entry.path))
480                            .chain(
481                                find_microsoft_store_pythons()
482                                    .filter(version_filter)
483                                    .map(|entry| (PythonSource::MicrosoftStore, entry.path)),
484                            )
485                    })
486                    .map_err(Error::from)
487            })
488            .flatten_ok(),
489        ),
490        Err(err) => Box::new(iter::once(Err(Error::from(err)))),
491    };
492
493    #[cfg(not(windows))]
494    let from_windows_registry: Box<
495        dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
496    > = Box::new(iter::empty());
497
498    match preference {
499        PythonPreference::OnlyManaged => {
500            // TODO(zanieb): Ideally, we'd create "fake" managed installation directories for tests,
501            // but for now... we'll just include the test interpreters which are always on the
502            // search path.
503            if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
504                Box::new(from_managed_installations.chain(from_search_path))
505            } else {
506                Box::new(from_managed_installations)
507            }
508        }
509        PythonPreference::Managed => Box::new(
510            from_managed_installations
511                .chain(from_search_path)
512                .chain(from_windows_registry),
513        ),
514        PythonPreference::System => Box::new(
515            from_search_path
516                .chain(from_windows_registry)
517                .chain(from_managed_installations),
518        ),
519        PythonPreference::OnlySystem => Box::new(from_search_path.chain(from_windows_registry)),
520    }
521}
522
523/// Lazily iterate over all discoverable Python executables.
524///
525/// Note that Python executables may be excluded by the given [`EnvironmentPreference`],
526/// [`PythonPreference`], and [`PlatformRequest`]. However, these filters are only applied for
527/// performance. We cannot guarantee that the all requests or preferences are satisfied until we
528/// query the interpreter.
529///
530/// See [`python_executables_from_installed`] and [`python_executables_from_virtual_environments`]
531/// for more information on discovery.
532fn python_executables<'a>(
533    version: &'a VersionRequest,
534    implementation: Option<&'a ImplementationName>,
535    platform: PlatformRequest,
536    environments: EnvironmentPreference,
537    preference: PythonPreference,
538) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
539    // Always read from `UV_INTERNAL__PARENT_INTERPRETER` — it could be a system interpreter
540    let from_parent_interpreter = iter::once_with(|| {
541        env::var_os(EnvVars::UV_INTERNAL__PARENT_INTERPRETER)
542            .into_iter()
543            .map(|path| Ok((PythonSource::ParentInterpreter, PathBuf::from(path))))
544    })
545    .flatten();
546
547    // Check if the base conda environment is active
548    let from_base_conda_environment = iter::once_with(move || {
549        conda_environment_from_env(CondaEnvironmentKind::Base)
550            .into_iter()
551            .map(virtualenv_python_executable)
552            .map(|path| Ok((PythonSource::BaseCondaPrefix, path)))
553    })
554    .flatten();
555
556    let from_virtual_environments = python_executables_from_virtual_environments();
557    let from_installed =
558        python_executables_from_installed(version, implementation, platform, preference);
559
560    // Limit the search to the relevant environment preference; this avoids unnecessary work like
561    // traversal of the file system. Subsequent filtering should be done by the caller with
562    // `source_satisfies_environment_preference` and `EnvironmentPreference::allows_installation`.
563    match environments {
564        EnvironmentPreference::OnlyVirtual => {
565            Box::new(from_parent_interpreter.chain(from_virtual_environments))
566        }
567        EnvironmentPreference::ExplicitSystem | EnvironmentPreference::Any => Box::new(
568            from_parent_interpreter
569                .chain(from_virtual_environments)
570                .chain(from_base_conda_environment)
571                .chain(from_installed),
572        ),
573        EnvironmentPreference::OnlySystem => Box::new(
574            from_parent_interpreter
575                .chain(from_base_conda_environment)
576                .chain(from_installed),
577        ),
578    }
579}
580
581/// Lazily iterate over Python executables in the `PATH`.
582///
583/// The [`VersionRequest`] and [`ImplementationName`] are used to determine the possible
584/// Python interpreter names, e.g. if looking for Python 3.9 we will look for `python3.9`
585/// or if looking for `PyPy` we will look for `pypy` in addition to the default names.
586///
587/// Executables are returned in the search path order, then by specificity of the name, e.g.
588/// `python3.9` is preferred over `python3` and `pypy3.9` is preferred over `python3.9`.
589///
590/// If a `version` is not provided, we will only look for default executable names e.g.
591/// `python3` and `python` — `python3.9` and similar will not be included.
592fn python_executables_from_search_path<'a>(
593    version: &'a VersionRequest,
594    implementation: Option<&'a ImplementationName>,
595) -> impl Iterator<Item = PathBuf> + 'a {
596    // `UV_PYTHON_SEARCH_PATH` can be used to override `PATH` for Python executable discovery
597    let search_path = env::var_os(EnvVars::UV_PYTHON_SEARCH_PATH)
598        .unwrap_or(env::var_os(EnvVars::PATH).unwrap_or_default());
599
600    let possible_names: Vec<_> = version
601        .executable_names(implementation)
602        .into_iter()
603        .map(|name| name.to_string())
604        .collect();
605
606    trace!(
607        "Searching PATH for executables: {}",
608        possible_names.join(", ")
609    );
610
611    // Split and iterate over the paths instead of using `which_all` so we can
612    // check multiple names per directory while respecting the search path order and python names
613    // precedence.
614    let search_dirs: Vec<_> = env::split_paths(&search_path).collect();
615    let mut seen_dirs = FxHashSet::with_capacity_and_hasher(search_dirs.len(), FxBuildHasher);
616    search_dirs
617        .into_iter()
618        .filter(|dir| dir.is_dir())
619        .flat_map(move |dir| {
620            // Clone the directory for second closure
621            let dir_clone = dir.clone();
622            trace!(
623                "Checking `PATH` directory for interpreters: {}",
624                dir.display()
625            );
626            same_file::Handle::from_path(&dir)
627                // Skip directories we've already seen, to avoid inspecting interpreters multiple
628                // times when directories are repeated or symlinked in the `PATH`
629                .map(|handle| seen_dirs.insert(handle))
630                .inspect(|fresh_dir| {
631                    if !fresh_dir {
632                        trace!("Skipping already seen directory: {}", dir.display());
633                    }
634                })
635                // If we cannot determine if the directory is unique, we'll assume it is
636                .unwrap_or(true)
637                .then(|| {
638                    possible_names
639                        .clone()
640                        .into_iter()
641                        .flat_map(move |name| {
642                            // Since we're just working with a single directory at a time, we collect to simplify ownership
643                            which::which_in_global(&*name, Some(&dir))
644                                .into_iter()
645                                .flatten()
646                                // We have to collect since `which` requires that the regex outlives its
647                                // parameters, and the dir is local while we return the iterator.
648                                .collect::<Vec<_>>()
649                        })
650                        .chain(find_all_minor(implementation, version, &dir_clone))
651                        .filter(|path| !is_windows_store_shim(path))
652                        .inspect(|path| {
653                            trace!("Found possible Python executable: {}", path.display());
654                        })
655                        .chain(
656                            // TODO(zanieb): Consider moving `python.bat` into `possible_names` to avoid a chain
657                            cfg!(windows)
658                                .then(move || {
659                                    which::which_in_global("python.bat", Some(&dir_clone))
660                                        .into_iter()
661                                        .flatten()
662                                        .collect::<Vec<_>>()
663                                })
664                                .into_iter()
665                                .flatten(),
666                        )
667                })
668                .into_iter()
669                .flatten()
670        })
671}
672
673/// Find all acceptable `python3.x` minor versions.
674///
675/// For example, let's say `python` and `python3` are Python 3.10. When a user requests `>= 3.11`,
676/// we still need to find a `python3.12` in PATH.
677fn find_all_minor(
678    implementation: Option<&ImplementationName>,
679    version_request: &VersionRequest,
680    dir: &Path,
681) -> impl Iterator<Item = PathBuf> + use<> {
682    match version_request {
683        &VersionRequest::Any
684        | VersionRequest::Default
685        | VersionRequest::Major(_, _)
686        | VersionRequest::Range(_, _) => {
687            let regex = if let Some(implementation) = implementation {
688                Regex::new(&format!(
689                    r"^({}|python3)\.(?<minor>\d\d?)t?{}$",
690                    regex::escape(&implementation.to_string()),
691                    regex::escape(EXE_SUFFIX)
692                ))
693                .unwrap()
694            } else {
695                Regex::new(&format!(
696                    r"^python3\.(?<minor>\d\d?)t?{}$",
697                    regex::escape(EXE_SUFFIX)
698                ))
699                .unwrap()
700            };
701            let all_minors = fs_err::read_dir(dir)
702                .into_iter()
703                .flatten()
704                .flatten()
705                .map(|entry| entry.path())
706                .filter(move |path| {
707                    let Some(filename) = path.file_name() else {
708                        return false;
709                    };
710                    let Some(filename) = filename.to_str() else {
711                        return false;
712                    };
713                    let Some(captures) = regex.captures(filename) else {
714                        return false;
715                    };
716
717                    // Filter out interpreter we already know have a too low minor version.
718                    let minor = captures["minor"].parse().ok();
719                    if let Some(minor) = minor {
720                        // Optimization: Skip generally unsupported Python versions without querying.
721                        if minor < 6 {
722                            return false;
723                        }
724                        // Optimization 2: Skip excluded Python (minor) versions without querying.
725                        if !version_request.matches_major_minor(3, minor) {
726                            return false;
727                        }
728                    }
729                    true
730                })
731                .filter(|path| is_executable(path))
732                .collect::<Vec<_>>();
733            Either::Left(all_minors.into_iter())
734        }
735        VersionRequest::MajorMinor(_, _, _)
736        | VersionRequest::MajorMinorPatch(_, _, _, _)
737        | VersionRequest::MajorMinorPrerelease(_, _, _, _)
738        | VersionRequest::MajorMinorPatchPrerelease(_, _, _, _, _) => Either::Right(iter::empty()),
739    }
740}
741
742/// How to query discovered Python executables.
743#[derive(Debug, Clone, Copy)]
744enum QueryStrategy {
745    /// Query each executable as it is requested by the consumer.
746    Sequential,
747    /// Query all executables concurrently before yielding results.
748    Parallel,
749}
750
751/// Iterate over all discoverable Python interpreters.
752///
753/// Note interpreters may be excluded by the given [`EnvironmentPreference`], [`PythonPreference`],
754/// [`VersionRequest`], or [`PlatformRequest`].
755///
756/// The [`PlatformRequest`] is currently only applied to managed Python installations before querying
757/// the interpreter. The caller is responsible for ensuring it is applied otherwise.
758///
759/// See [`python_executables`] for more information on discovery.
760fn python_installations<'a>(
761    version: &'a VersionRequest,
762    implementation: Option<&'a ImplementationName>,
763    platform: PlatformRequest,
764    environments: EnvironmentPreference,
765    preference: PythonPreference,
766    cache: &'a Cache,
767    strategy: QueryStrategy,
768) -> Box<dyn Iterator<Item = Result<PythonInstallation, Error>> + 'a> {
769    Box::new(
770        python_installations_from_executables(
771            // Perform filtering on the discovered executables based on their source. This avoids
772            // unnecessary interpreter queries, which are generally expensive. We'll filter again
773            // with `PythonInstallation::satisfies_preferences` after querying.
774            python_executables(version, implementation, platform, environments, preference)
775                .filter_ok(move |(source, path)| {
776                    source_satisfies_environment_preference(*source, path, environments)
777                }),
778            cache,
779            strategy,
780        )
781        .filter_ok(move |installation| {
782            installation.satisfies_preferences(version, environments, preference)
783        })
784        .map_ok(PythonInstallation::maybe_with_test_source),
785    )
786}
787
788/// Query a single Python executable, returning a [`PythonInstallation`] on success.
789fn python_installation_from_executable(
790    source: PythonSource,
791    path: PathBuf,
792    cache: &Cache,
793) -> Result<PythonInstallation, Error> {
794    Interpreter::query(&path, cache)
795        .map(|interpreter| PythonInstallation {
796            source,
797            interpreter,
798        })
799        .inspect(|installation| {
800            debug!(
801                "Found `{}` at `{}` ({source})",
802                installation.key(),
803                path.display()
804            );
805        })
806        .map_err(|err| Error::Query(Box::new(err), path, source))
807        .inspect_err(|err| debug!("{err}"))
808}
809
810/// Convert Python executables into installations using the given query strategy.
811fn python_installations_from_executables<'a>(
812    executables: impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
813    cache: &'a Cache,
814    strategy: QueryStrategy,
815) -> Box<dyn Iterator<Item = Result<PythonInstallation, Error>> + 'a> {
816    match strategy {
817        QueryStrategy::Sequential => Box::new(executables.map(move |result| match result {
818            Ok((source, path)) => python_installation_from_executable(source, path, cache),
819            Err(err) => Err(err),
820        })),
821        QueryStrategy::Parallel => {
822            let items: Vec<Result<(PythonSource, PathBuf), Error>> = executables.collect();
823            let results: Vec<Result<PythonInstallation, Error>> = items
824                .into_par_iter()
825                .map(|result| match result {
826                    Ok((source, path)) => python_installation_from_executable(source, path, cache),
827                    Err(err) => Err(err),
828                })
829                .collect();
830            Box::new(results.into_iter())
831        }
832    }
833}
834
835/// Whether a [`Interpreter`] matches the [`EnvironmentPreference`].
836///
837/// This is the correct way to determine if an interpreter matches the preference. In contrast,
838/// [`source_satisfies_environment_preference`] only checks if a [`PythonSource`] **could** satisfy
839/// preference as a pre-filtering step. We cannot definitively know if a Python interpreter is in
840/// a virtual environment until we query it.
841fn interpreter_satisfies_environment_preference(
842    source: PythonSource,
843    interpreter: &Interpreter,
844    preference: EnvironmentPreference,
845) -> bool {
846    match (
847        preference,
848        // Conda environments are not conformant virtual environments but we treat them as such.
849        interpreter.is_virtualenv() || (matches!(source, PythonSource::CondaPrefix)),
850    ) {
851        (EnvironmentPreference::Any, _) => true,
852        (EnvironmentPreference::OnlyVirtual, true) => true,
853        (EnvironmentPreference::OnlyVirtual, false) => {
854            debug!(
855                "Ignoring Python interpreter at `{}`: only virtual environments allowed",
856                interpreter.sys_executable().display()
857            );
858            false
859        }
860        (EnvironmentPreference::ExplicitSystem, true) => true,
861        (EnvironmentPreference::ExplicitSystem, false) => {
862            if matches!(
863                source,
864                PythonSource::ProvidedPath | PythonSource::ParentInterpreter
865            ) {
866                debug!(
867                    "Allowing explicitly requested system Python interpreter at `{}`",
868                    interpreter.sys_executable().display()
869                );
870                true
871            } else {
872                debug!(
873                    "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
874                    interpreter.sys_executable().display()
875                );
876                false
877            }
878        }
879        (EnvironmentPreference::OnlySystem, true) => {
880            debug!(
881                "Ignoring Python interpreter at `{}`: system interpreter required",
882                interpreter.sys_executable().display()
883            );
884            false
885        }
886        (EnvironmentPreference::OnlySystem, false) => true,
887    }
888}
889
890/// Returns true if a [`PythonSource`] could satisfy the [`EnvironmentPreference`].
891///
892/// This is useful as a pre-filtering step. Use of [`EnvironmentPreference::allows_installation`]
893/// is required to determine if an [`Interpreter`] satisfies the preference.
894///
895/// The interpreter path is only used for debug messages.
896fn source_satisfies_environment_preference(
897    source: PythonSource,
898    interpreter_path: &Path,
899    preference: EnvironmentPreference,
900) -> bool {
901    match preference {
902        EnvironmentPreference::Any => true,
903        EnvironmentPreference::OnlyVirtual => {
904            if source.is_maybe_virtualenv() {
905                true
906            } else {
907                debug!(
908                    "Ignoring Python interpreter at `{}`: only virtual environments allowed",
909                    interpreter_path.display()
910                );
911                false
912            }
913        }
914        EnvironmentPreference::ExplicitSystem => {
915            if source.is_maybe_virtualenv() {
916                true
917            } else {
918                debug!(
919                    "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
920                    interpreter_path.display()
921                );
922                false
923            }
924        }
925        EnvironmentPreference::OnlySystem => {
926            if source.is_maybe_system() {
927                true
928            } else {
929                debug!(
930                    "Ignoring Python interpreter at `{}`: system interpreter required",
931                    interpreter_path.display()
932                );
933                false
934            }
935        }
936    }
937}
938
939/// Check if an encountered error is critical and should stop discovery.
940///
941/// Returns false when an error could be due to a faulty Python installation and we should continue searching for a working one.
942impl Error {
943    pub fn is_critical(&self) -> bool {
944        match self {
945            // When querying the Python interpreter fails, we will only raise errors that demonstrate that something is broken
946            // If the Python interpreter returned a bad response, we'll continue searching for one that works
947            Self::Query(err, _, source) => match &**err {
948                InterpreterError::Encode(_)
949                | InterpreterError::Io(_)
950                | InterpreterError::SpawnFailed { .. } => true,
951                InterpreterError::UnexpectedResponse(UnexpectedResponseError { path, .. })
952                | InterpreterError::StatusCode(StatusCodeError { path, .. }) => {
953                    debug!(
954                        "Skipping bad interpreter at {} from {source}: {err}",
955                        path.display()
956                    );
957                    false
958                }
959                InterpreterError::QueryScript { path, err } => {
960                    debug!(
961                        "Skipping bad interpreter at {} from {source}: {err}",
962                        path.display()
963                    );
964                    false
965                }
966                #[cfg(windows)]
967                InterpreterError::CorruptWindowsPackage { path, err } => {
968                    debug!(
969                        "Skipping bad interpreter at {} from {source}: {err}",
970                        path.display()
971                    );
972                    false
973                }
974                InterpreterError::PermissionDenied { path, err } => {
975                    debug!(
976                        "Skipping unexecutable interpreter at {} from {source}: {err}",
977                        path.display()
978                    );
979                    false
980                }
981                InterpreterError::NotFound(path)
982                | InterpreterError::BrokenLink(BrokenLink { path, .. }) => {
983                    // If the interpreter is from an active, valid virtual environment, we should
984                    // fail because it's broken
985                    if matches!(source, PythonSource::ActiveEnvironment)
986                        && uv_fs::is_virtualenv_executable(path)
987                    {
988                        true
989                    } else {
990                        trace!("Skipping missing interpreter at {}", path.display());
991                        false
992                    }
993                }
994            },
995            Self::VirtualEnv(VirtualEnvError::MissingPyVenvCfg(path)) => {
996                trace!("Skipping broken virtualenv at {}", path.display());
997                false
998            }
999            _ => true,
1000        }
1001    }
1002}
1003
1004/// Create a [`PythonInstallation`] from a Python installation root directory.
1005fn python_installation_from_directory(
1006    path: &PathBuf,
1007    cache: &Cache,
1008) -> Result<PythonInstallation, crate::interpreter::Error> {
1009    let executable = virtualenv_python_executable(path);
1010    Ok(PythonInstallation {
1011        source: PythonSource::ProvidedPath,
1012        interpreter: Interpreter::query(&executable, cache)?,
1013    })
1014}
1015
1016/// Lazily iterate over all Python executable paths on the path with the given executable name.
1017fn python_executables_with_name(
1018    name: &str,
1019) -> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + '_ {
1020    which_all(name)
1021        .into_iter()
1022        .flat_map(|inner| inner.map(|path| Ok((PythonSource::SearchPath, path))))
1023}
1024
1025/// Lazily iterate over all Python installations on the path with the given executable name.
1026fn python_installations_with_name<'a>(
1027    name: &'a str,
1028    cache: &'a Cache,
1029    strategy: QueryStrategy,
1030) -> Box<dyn Iterator<Item = Result<PythonInstallation, Error>> + 'a> {
1031    python_installations_from_executables(python_executables_with_name(name), cache, strategy)
1032}
1033
1034/// Iterate over all Python installations that satisfy the given request.
1035pub fn find_python_installations<'a>(
1036    request: &'a PythonRequest,
1037    environments: EnvironmentPreference,
1038    preference: PythonPreference,
1039    cache: &'a Cache,
1040) -> Box<dyn Iterator<Item = Result<FindPythonResult, Error>> + 'a> {
1041    find_python_installations_with_strategy(
1042        request,
1043        environments,
1044        preference,
1045        cache,
1046        QueryStrategy::Sequential,
1047    )
1048}
1049
1050/// Iterate over all Python installations that satisfy the given request using the given query
1051/// strategy.
1052fn find_python_installations_with_strategy<'a>(
1053    request: &'a PythonRequest,
1054    environments: EnvironmentPreference,
1055    preference: PythonPreference,
1056    cache: &'a Cache,
1057    strategy: QueryStrategy,
1058) -> Box<dyn Iterator<Item = Result<FindPythonResult, Error>> + 'a> {
1059    let sources = DiscoveryPreferences {
1060        python_preference: preference,
1061        environment_preference: environments,
1062    }
1063    .sources(request);
1064
1065    match request {
1066        PythonRequest::File(path) => Box::new(iter::once({
1067            if preference.allows_source(PythonSource::ProvidedPath) {
1068                debug!("Checking for Python interpreter at {request}");
1069                match Interpreter::query(path, cache) {
1070                    Ok(interpreter) => Ok(Ok(PythonInstallation {
1071                        source: PythonSource::ProvidedPath,
1072                        interpreter,
1073                    })),
1074                    Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1075                        Ok(Err(PythonNotFound {
1076                            request: request.clone(),
1077                            python_preference: preference,
1078                            environment_preference: environments,
1079                        }))
1080                    }
1081                    Err(err) => Err(Error::Query(
1082                        Box::new(err),
1083                        path.clone(),
1084                        PythonSource::ProvidedPath,
1085                    )),
1086                }
1087            } else {
1088                Err(Error::SourceNotAllowed(
1089                    request.clone(),
1090                    PythonSource::ProvidedPath,
1091                    preference,
1092                ))
1093            }
1094        })),
1095        PythonRequest::Directory(path) => Box::new(iter::once({
1096            if preference.allows_source(PythonSource::ProvidedPath) {
1097                debug!("Checking for Python interpreter in {request}");
1098                match python_installation_from_directory(path, cache) {
1099                    Ok(installation) => Ok(Ok(installation)),
1100                    Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1101                        Ok(Err(PythonNotFound {
1102                            request: request.clone(),
1103                            python_preference: preference,
1104                            environment_preference: environments,
1105                        }))
1106                    }
1107                    Err(err) => Err(Error::Query(
1108                        Box::new(err),
1109                        path.clone(),
1110                        PythonSource::ProvidedPath,
1111                    )),
1112                }
1113            } else {
1114                Err(Error::SourceNotAllowed(
1115                    request.clone(),
1116                    PythonSource::ProvidedPath,
1117                    preference,
1118                ))
1119            }
1120        })),
1121        PythonRequest::ExecutableName(name) => {
1122            if preference.allows_source(PythonSource::SearchPath) {
1123                debug!("Searching for Python interpreter with {request}");
1124                Box::new(
1125                    python_installations_with_name(name, cache, strategy)
1126                        .filter_ok(move |installation| {
1127                            environments.allows_installation(installation)
1128                        })
1129                        .map_ok(Ok),
1130                )
1131            } else {
1132                Box::new(iter::once(Err(Error::SourceNotAllowed(
1133                    request.clone(),
1134                    PythonSource::SearchPath,
1135                    preference,
1136                ))))
1137            }
1138        }
1139        PythonRequest::Any => Box::new({
1140            debug!("Searching for any Python interpreter in {sources}");
1141            python_installations(
1142                &VersionRequest::Any,
1143                None,
1144                PlatformRequest::default(),
1145                environments,
1146                preference,
1147                cache,
1148                strategy,
1149            )
1150            .map_ok(Ok)
1151        }),
1152        PythonRequest::Default => Box::new({
1153            debug!("Searching for default Python interpreter in {sources}");
1154            python_installations(
1155                &VersionRequest::Default,
1156                None,
1157                PlatformRequest::default(),
1158                environments,
1159                preference,
1160                cache,
1161                strategy,
1162            )
1163            .map_ok(Ok)
1164        }),
1165        PythonRequest::Version(version) => {
1166            if let Err(err) = version.check_supported() {
1167                return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1168            }
1169            Box::new({
1170                debug!("Searching for {request} in {sources}");
1171                python_installations(
1172                    version,
1173                    None,
1174                    PlatformRequest::default(),
1175                    environments,
1176                    preference,
1177                    cache,
1178                    strategy,
1179                )
1180                .map_ok(Ok)
1181            })
1182        }
1183        PythonRequest::Implementation(implementation) => Box::new({
1184            debug!("Searching for a {request} interpreter in {sources}");
1185            python_installations(
1186                &VersionRequest::Default,
1187                Some(implementation),
1188                PlatformRequest::default(),
1189                environments,
1190                preference,
1191                cache,
1192                strategy,
1193            )
1194            .filter_ok(|installation| implementation.matches_interpreter(&installation.interpreter))
1195            .map_ok(Ok)
1196        }),
1197        PythonRequest::ImplementationVersion(implementation, version) => {
1198            if let Err(err) = version.check_supported() {
1199                return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1200            }
1201            Box::new({
1202                debug!("Searching for {request} in {sources}");
1203                python_installations(
1204                    version,
1205                    Some(implementation),
1206                    PlatformRequest::default(),
1207                    environments,
1208                    preference,
1209                    cache,
1210                    strategy,
1211                )
1212                .filter_ok(|installation| {
1213                    implementation.matches_interpreter(&installation.interpreter)
1214                })
1215                .map_ok(Ok)
1216            })
1217        }
1218        PythonRequest::Key(request) => {
1219            if let Some(version) = request.version() {
1220                if let Err(err) = version.check_supported() {
1221                    return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1222                }
1223            }
1224
1225            Box::new({
1226                debug!("Searching for {request} in {sources}");
1227                python_installations(
1228                    request.version().unwrap_or(&VersionRequest::Default),
1229                    request.implementation(),
1230                    request.platform(),
1231                    environments,
1232                    preference,
1233                    cache,
1234                    strategy,
1235                )
1236                .filter_ok(move |installation| {
1237                    request.satisfied_by_interpreter(&installation.interpreter)
1238                })
1239                .map_ok(Ok)
1240            })
1241        }
1242    }
1243}
1244
1245/// Find all Python installations that satisfy the given request, querying interpreters
1246/// concurrently.
1247///
1248/// Unlike [`find_python_installations`], this eagerly collects matching installations instead of
1249/// returning a lazy iterator. Non-critical discovery errors are dropped, while critical errors are
1250/// propagated in discovery order.
1251pub fn find_all_python_installations(
1252    request: &PythonRequest,
1253    environments: EnvironmentPreference,
1254    preference: PythonPreference,
1255    cache: &Cache,
1256) -> Result<Vec<PythonInstallation>, Error> {
1257    let results = find_python_installations_with_strategy(
1258        request,
1259        environments,
1260        preference,
1261        cache,
1262        QueryStrategy::Parallel,
1263    );
1264    let mut installations = Vec::new();
1265    for result in results {
1266        match result {
1267            Ok(Ok(installation)) => installations.push(installation),
1268            Ok(Err(_)) => {}
1269            Err(err) if err.is_critical() => return Err(err),
1270            Err(_) => {}
1271        }
1272    }
1273    Ok(installations)
1274}
1275
1276/// Find a Python installation that satisfies the given request.
1277///
1278/// If an error is encountered while locating or inspecting a candidate installation,
1279/// the error will raised instead of attempting further candidates.
1280pub(crate) fn find_python_installation(
1281    request: &PythonRequest,
1282    environments: EnvironmentPreference,
1283    preference: PythonPreference,
1284    cache: &Cache,
1285) -> Result<FindPythonResult, Error> {
1286    let installations = find_python_installations(request, environments, preference, cache);
1287    let mut first_prerelease = None;
1288    let mut first_debug = None;
1289    let mut first_managed = None;
1290    let mut first_error = None;
1291    for result in installations {
1292        // Iterate until the first critical error or happy result
1293        if !result.as_ref().err().is_none_or(Error::is_critical) {
1294            // Track the first non-critical error
1295            if first_error.is_none() {
1296                if let Err(err) = result {
1297                    first_error = Some(err);
1298                }
1299            }
1300            continue;
1301        }
1302
1303        // If it's an error, we're done.
1304        let Ok(Ok(ref installation)) = result else {
1305            return result;
1306        };
1307
1308        // Check if we need to skip the interpreter because it is "not allowed", e.g., if it is a
1309        // pre-release version or an alternative implementation, using it requires opt-in.
1310
1311        // If the interpreter has a default executable name, e.g. `python`, and was found on the
1312        // search path, we consider this opt-in to use it.
1313        let has_default_executable_name = installation.interpreter.has_default_executable_name()
1314            && matches!(
1315                installation.source,
1316                PythonSource::SearchPath | PythonSource::SearchPathFirst
1317            );
1318
1319        // If it's a pre-release and pre-releases aren't allowed, skip it — but store it for later
1320        // since we'll use a pre-release if no other versions are available.
1321        if installation.python_version().pre().is_some()
1322            && !request.allows_prereleases()
1323            && !installation.source.allows_prereleases()
1324            && !has_default_executable_name
1325        {
1326            debug!("Skipping pre-release installation {}", installation.key());
1327            if first_prerelease.is_none() {
1328                first_prerelease = Some(installation.clone());
1329            }
1330            continue;
1331        }
1332
1333        // If it's a debug build and debug builds aren't allowed, skip it — but store it for later
1334        // since we'll use a debug build if no other versions are available.
1335        if installation.key().variant().is_debug()
1336            && !request.allows_debug()
1337            && !installation.source.allows_debug()
1338            && !has_default_executable_name
1339        {
1340            debug!("Skipping debug installation {}", installation.key());
1341            if first_debug.is_none() {
1342                first_debug = Some(installation.clone());
1343            }
1344            continue;
1345        }
1346
1347        // If it's an alternative implementation and alternative implementations aren't allowed,
1348        // skip it. Note we avoid querying these interpreters at all if they're on the search path
1349        // and are not requested, but other sources such as the managed installations can include
1350        // them.
1351        if installation.is_alternative_implementation()
1352            && !request.allows_alternative_implementations()
1353            && !installation.source.allows_alternative_implementations()
1354            && !has_default_executable_name
1355        {
1356            debug!("Skipping alternative implementation {}", installation.key());
1357            continue;
1358        }
1359
1360        // If it's a managed Python installation, and system interpreters are preferred, skip it
1361        // for now.
1362        if matches!(preference, PythonPreference::System) && installation.is_managed() {
1363            debug!(
1364                "Skipping managed installation {}: system installation preferred",
1365                installation.key()
1366            );
1367            if first_managed.is_none() {
1368                first_managed = Some(installation.clone());
1369            }
1370            continue;
1371        }
1372
1373        // If we didn't skip it, this is the installation to use
1374        return result;
1375    }
1376
1377    // If we only found managed installations, and the preference allows them, we should return
1378    // the first one.
1379    if let Some(installation) = first_managed {
1380        debug!(
1381            "Allowing managed installation {}: no system installations",
1382            installation.key()
1383        );
1384        return Ok(Ok(installation));
1385    }
1386
1387    // If we only found debug installations, they're implicitly allowed and we should return the
1388    // first one.
1389    if let Some(installation) = first_debug {
1390        debug!(
1391            "Allowing debug installation {}: no non-debug installations",
1392            installation.key()
1393        );
1394        return Ok(Ok(installation));
1395    }
1396
1397    // If we only found pre-releases, they're implicitly allowed and we should return the first one.
1398    if let Some(installation) = first_prerelease {
1399        debug!(
1400            "Allowing pre-release installation {}: no stable installations",
1401            installation.key()
1402        );
1403        return Ok(Ok(installation));
1404    }
1405
1406    // If we found a Python, but it was unusable for some reason, report that instead of saying we
1407    // couldn't find any Python interpreters.
1408    if let Some(err) = first_error {
1409        return Err(err);
1410    }
1411
1412    Ok(Err(PythonNotFound {
1413        request: request.clone(),
1414        environment_preference: environments,
1415        python_preference: preference,
1416    }))
1417}
1418
1419/// Find the best-matching Python installation.
1420///
1421/// If no Python version is provided, we will use the first available installation.
1422///
1423/// If a Python version is provided, we will first try to find an exact match. If
1424/// that cannot be found and a patch version was requested, we will look for a match
1425/// without comparing the patch version number. If that cannot be found, we fall back to
1426/// the first available version.
1427///
1428/// At all points, if the specified version cannot be found, we will attempt to
1429/// download it if downloads are enabled.
1430///
1431/// See [`find_python_installation`] for more details on installation discovery.
1432#[instrument(skip_all, fields(request))]
1433pub(crate) async fn find_best_python_installation(
1434    request: &PythonRequest,
1435    environments: EnvironmentPreference,
1436    preference: PythonPreference,
1437    downloads_enabled: bool,
1438    client_builder: &BaseClientBuilder<'_>,
1439    cache: &Cache,
1440    reporter: Option<&dyn crate::downloads::Reporter>,
1441    python_install_mirror: Option<&str>,
1442    pypy_install_mirror: Option<&str>,
1443    python_downloads_json_url: Option<&str>,
1444) -> Result<PythonInstallation, crate::Error> {
1445    debug!("Starting Python discovery for {request}");
1446    let original_request = request;
1447
1448    let mut previous_fetch_failed = false;
1449    let mut download_state = None;
1450
1451    let request_without_patch = match request {
1452        PythonRequest::Version(version) => {
1453            if version.has_patch() {
1454                Some(PythonRequest::Version(version.clone().without_patch()))
1455            } else {
1456                None
1457            }
1458        }
1459        PythonRequest::ImplementationVersion(implementation, version) => Some(
1460            PythonRequest::ImplementationVersion(*implementation, version.clone().without_patch()),
1461        ),
1462        _ => None,
1463    };
1464
1465    for (attempt, request) in iter::once(original_request)
1466        .chain(request_without_patch.iter())
1467        .chain(iter::once(&PythonRequest::Default))
1468        .enumerate()
1469    {
1470        debug!(
1471            "Looking for {request}{}",
1472            if request != original_request {
1473                format!(" attempt {attempt} (fallback after failing to find: {original_request})")
1474            } else {
1475                String::new()
1476            }
1477        );
1478        let result = find_python_installation(request, environments, preference, cache);
1479        let error = match result {
1480            Ok(Ok(installation)) => {
1481                warn_on_unsupported_python(installation.interpreter());
1482                return Ok(installation);
1483            }
1484            // Continue if we can't find a matching Python and ignore non-critical discovery errors
1485            Ok(Err(error)) => error.into(),
1486            Err(error) if !error.is_critical() => error.into(),
1487            Err(error) => return Err(error.into()),
1488        };
1489
1490        // Attempt to download the version if downloads are enabled
1491        if downloads_enabled
1492            && !previous_fetch_failed
1493            && let Some(download_request) = PythonDownloadRequest::from_request(request)
1494        {
1495            let (client, retry_policy, download_list) =
1496                if let Some(download_state) = &mut download_state {
1497                    download_state
1498                } else {
1499                    let download_list_client = client_builder.build()?;
1500                    let download_list = ManagedPythonDownloadList::new(
1501                        &download_list_client,
1502                        python_downloads_json_url,
1503                    )
1504                    .await?;
1505                    let retry_policy = client_builder.retry_policy();
1506
1507                    // Python downloads are performing their own retries to catch stream errors, disable
1508                    // the default retries to avoid the middleware performing uncontrolled retries.
1509                    let client = client_builder.clone().retries(0).build()?;
1510                    download_state.insert((client, retry_policy, download_list))
1511                };
1512
1513            let download = download_request
1514                .clone()
1515                .fill()
1516                .map(|request| download_list.find(&request));
1517
1518            let result = match download {
1519                Ok(Ok(download)) => PythonInstallation::fetch(
1520                    download,
1521                    client,
1522                    retry_policy,
1523                    cache,
1524                    reporter,
1525                    python_install_mirror,
1526                    pypy_install_mirror,
1527                )
1528                .await
1529                .map(Some),
1530                Ok(Err(crate::downloads::Error::NoDownloadFound(_))) => Ok(None),
1531                Ok(Err(error)) => Err(error.into()),
1532                Err(error) => Err(error.into()),
1533            };
1534            if let Ok(Some(installation)) = result {
1535                return Ok(installation);
1536            }
1537            // Emit a warning instead of failing since we may find a suitable
1538            // interpreter on the system after relaxing the request further.
1539            // Additionally, uv did not previously attempt downloads in this
1540            // code path and we want to minimize the fatal cases for
1541            // backwards compatibility.
1542            // Errors encountered here are either network errors or quirky
1543            // configuration problems.
1544            if let Err(error) = result {
1545                // If the request was for the default or any version, propagate
1546                // the error as nothing else we are about to do will help the
1547                // situation.
1548                if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1549                    return Err(error);
1550                }
1551
1552                let error = anyhow::Error::from(error).context(format!(
1553                    "A managed Python download is available for {request}, but an error occurred when attempting to download it."
1554                ));
1555                write_warning_chain(error.as_ref()).expect("writing to stderr should not fail");
1556                previous_fetch_failed = true;
1557            }
1558        }
1559
1560        // If this was a request for the Default or Any version, this means that
1561        // either that's what we were called with, or we're on the last
1562        // iteration.
1563        //
1564        // The most recent find error therefore becomes a fatal one.
1565        if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1566            return Err(match error {
1567                crate::Error::MissingPython(err, _) => PythonNotFound {
1568                    // Use a more general error in this case since we looked for multiple versions
1569                    request: original_request.clone(),
1570                    python_preference: err.python_preference,
1571                    environment_preference: err.environment_preference,
1572                }
1573                .into(),
1574                other => other,
1575            });
1576        }
1577    }
1578
1579    unreachable!("The loop should have terminated when it reached PythonRequest::Default");
1580}
1581
1582/// Display a warning if the Python version of the [`Interpreter`] is unsupported by uv.
1583fn warn_on_unsupported_python(interpreter: &Interpreter) {
1584    // Warn on usage with an unsupported Python version
1585    if interpreter.python_tuple() < (3, 8) {
1586        warn_user_once!(
1587            "uv is only compatible with Python >=3.8, found Python {}",
1588            interpreter.python_version()
1589        );
1590    }
1591}
1592
1593/// On Windows we might encounter the Windows Store proxy shim (enabled in:
1594/// Settings/Apps/Advanced app settings/App execution aliases). When Python is _not_ installed
1595/// via the Windows Store, but the proxy shim is enabled, then executing `python.exe` or
1596/// `python3.exe` will redirect to the Windows Store installer.
1597///
1598/// We need to detect that these `python.exe` and `python3.exe` files are _not_ Python
1599/// executables.
1600///
1601/// This method is taken from Rye:
1602///
1603/// > This is a pretty dumb way.  We know how to parse this reparse point, but Microsoft
1604/// > does not want us to do this as the format is unstable.  So this is a best effort way.
1605/// > we just hope that the reparse point has the python redirector in it, when it's not
1606/// > pointing to a valid Python.
1607///
1608/// See: <https://github.com/astral-sh/rye/blob/b0e9eccf05fe4ff0ae7b0250a248c54f2d780b4d/rye/src/cli/shim.rs#L108>
1609#[cfg(windows)]
1610fn is_windows_store_shim(path: &Path) -> bool {
1611    use std::os::windows::fs::MetadataExt;
1612    use std::os::windows::prelude::OsStrExt;
1613    use windows::Win32::Foundation::CloseHandle;
1614    use windows::Win32::Storage::FileSystem::{
1615        CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
1616        FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_MODE, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
1617        OPEN_EXISTING,
1618    };
1619    use windows::Win32::System::IO::DeviceIoControl;
1620    use windows::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
1621    use windows::core::PCWSTR;
1622
1623    // The path must be absolute.
1624    if !path.is_absolute() {
1625        return false;
1626    }
1627
1628    // The path must point to something like:
1629    //   `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python3.exe`
1630    let mut components = path.components().rev();
1631
1632    // Ex) `python.exe`, `python3.exe`, `python3.12.exe`, etc.
1633    if !components
1634        .next()
1635        .and_then(|component| component.as_os_str().to_str())
1636        .is_some_and(|component| {
1637            component.starts_with("python")
1638                && std::path::Path::new(component)
1639                    .extension()
1640                    .is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
1641        })
1642    {
1643        return false;
1644    }
1645
1646    // Ex) `WindowsApps`
1647    if components
1648        .next()
1649        .is_none_or(|component| component.as_os_str() != "WindowsApps")
1650    {
1651        return false;
1652    }
1653
1654    // Ex) `Microsoft`
1655    if components
1656        .next()
1657        .is_none_or(|component| component.as_os_str() != "Microsoft")
1658    {
1659        return false;
1660    }
1661
1662    // The file is only relevant if it's a reparse point.
1663    let Ok(md) = fs_err::symlink_metadata(path) else {
1664        return false;
1665    };
1666    if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 {
1667        return false;
1668    }
1669
1670    let mut path_encoded = path
1671        .as_os_str()
1672        .encode_wide()
1673        .chain(std::iter::once(0))
1674        .collect::<Vec<_>>();
1675
1676    // SAFETY: The path is null-terminated.
1677    #[allow(unsafe_code)]
1678    let reparse_handle = unsafe {
1679        CreateFileW(
1680            PCWSTR(path_encoded.as_mut_ptr()),
1681            0,
1682            FILE_SHARE_MODE(0),
1683            None,
1684            OPEN_EXISTING,
1685            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
1686            None,
1687        )
1688    };
1689
1690    let Ok(reparse_handle) = reparse_handle else {
1691        return false;
1692    };
1693
1694    let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
1695    let mut bytes_returned = 0;
1696
1697    // SAFETY: The buffer is large enough to hold the reparse point.
1698    #[allow(unsafe_code, clippy::cast_possible_truncation)]
1699    let success = unsafe {
1700        DeviceIoControl(
1701            reparse_handle,
1702            FSCTL_GET_REPARSE_POINT,
1703            None,
1704            0,
1705            Some(buf.as_mut_ptr().cast()),
1706            buf.len() as u32 * 2,
1707            Some(&raw mut bytes_returned),
1708            None,
1709        )
1710        .is_ok()
1711    };
1712
1713    // SAFETY: The handle is valid.
1714    #[allow(unsafe_code)]
1715    unsafe {
1716        let _ = CloseHandle(reparse_handle);
1717    }
1718
1719    // If the operation failed, assume it's not a reparse point.
1720    if !success {
1721        return false;
1722    }
1723
1724    let reparse_point = String::from_utf16_lossy(&buf[..bytes_returned as usize]);
1725    reparse_point.contains("\\AppInstallerPythonRedirector.exe")
1726}
1727
1728/// On Unix, we do not need to deal with Windows store shims.
1729///
1730/// See the Windows implementation for details.
1731#[cfg(not(windows))]
1732fn is_windows_store_shim(_path: &Path) -> bool {
1733    false
1734}
1735
1736impl PythonVariant {
1737    fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
1738        match self {
1739            Self::Default => {
1740                // TODO(zanieb): Right now, we allow debug interpreters to be selected by default for
1741                // backwards compatibility, but we may want to change this in the future.
1742                if (interpreter.python_major(), interpreter.python_minor()) >= (3, 14) {
1743                    // For Python 3.14+, the free-threaded build is not considered experimental
1744                    // and can satisfy the default variant without opt-in
1745                    true
1746                } else {
1747                    // In Python 3.13 and earlier, the free-threaded build is considered
1748                    // experimental and requires explicit opt-in
1749                    !interpreter.gil_disabled()
1750                }
1751            }
1752            Self::Debug => interpreter.debug_enabled(),
1753            Self::Freethreaded => interpreter.gil_disabled(),
1754            Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
1755            Self::Gil => !interpreter.gil_disabled(),
1756            Self::GilDebug => !interpreter.gil_disabled() && interpreter.debug_enabled(),
1757        }
1758    }
1759
1760    /// Return the executable suffix for the variant, e.g., `t` for `python3.13t`.
1761    ///
1762    /// Returns an empty string for the default Python variant.
1763    pub fn executable_suffix(self) -> &'static str {
1764        match self {
1765            Self::Default => "",
1766            Self::Debug => "d",
1767            Self::Freethreaded => "t",
1768            Self::FreethreadedDebug => "td",
1769            Self::Gil => "",
1770            Self::GilDebug => "d",
1771        }
1772    }
1773
1774    /// Return the suffix for display purposes, e.g., `+gil`.
1775    pub fn display_suffix(self) -> &'static str {
1776        match self {
1777            Self::Default => "",
1778            Self::Debug => "+debug",
1779            Self::Freethreaded => "+freethreaded",
1780            Self::FreethreadedDebug => "+freethreaded+debug",
1781            Self::Gil => "+gil",
1782            Self::GilDebug => "+gil+debug",
1783        }
1784    }
1785
1786    /// Return the lib suffix for the variant, e.g., `t` for `python3.13t` but an empty string for
1787    /// `python3.13d` or `python3.13`.
1788    pub(crate) fn lib_suffix(self) -> &'static str {
1789        match self {
1790            Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "",
1791            Self::Freethreaded | Self::FreethreadedDebug => "t",
1792        }
1793    }
1794
1795    fn is_freethreaded(self) -> bool {
1796        match self {
1797            Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false,
1798            Self::Freethreaded | Self::FreethreadedDebug => true,
1799        }
1800    }
1801
1802    pub fn is_debug(self) -> bool {
1803        match self {
1804            Self::Default | Self::Freethreaded | Self::Gil => false,
1805            Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true,
1806        }
1807    }
1808}
1809impl PythonRequest {
1810    /// Create a request from a `Requires-Python` constraint.
1811    pub fn from_requires_python(requires_python: &RequiresPython) -> Option<Self> {
1812        let specifiers = requires_python.specifiers().clone();
1813        if specifiers.is_empty() {
1814            return None;
1815        }
1816
1817        Some(Self::Version(VersionRequest::from_specifiers(
1818            specifiers,
1819            PythonVariant::Default,
1820        )))
1821    }
1822
1823    /// Create a request from a string.
1824    ///
1825    /// This cannot fail, which means weird inputs will be parsed as [`PythonRequest::File`] or
1826    /// [`PythonRequest::ExecutableName`].
1827    ///
1828    /// This is intended for parsing the argument to the `--python` flag. See also
1829    /// [`try_from_tool_name`][Self::try_from_tool_name] below.
1830    pub fn parse(value: &str) -> Self {
1831        let lowercase_value = &value.to_ascii_lowercase();
1832
1833        // Literals, e.g. `any` or `default`
1834        if lowercase_value == "any" {
1835            return Self::Any;
1836        }
1837        if lowercase_value == "default" {
1838            return Self::Default;
1839        }
1840
1841        // the prefix of e.g. `python312` and the empty prefix of bare versions, e.g. `312`
1842        let abstract_version_prefixes = ["python", ""];
1843        let all_implementation_names =
1844            ImplementationName::long_names().chain(ImplementationName::short_names());
1845        // Abstract versions like `python@312`, `python312`, or `312`, plus implementations and
1846        // implementation versions like `pypy`, `pypy@312` or `pypy312`.
1847        if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1848            abstract_version_prefixes,
1849            all_implementation_names,
1850            lowercase_value,
1851        ) {
1852            return request;
1853        }
1854
1855        let value_as_path = PathBuf::from(value);
1856        // e.g. /path/to/.venv
1857        if value_as_path.is_dir() {
1858            return Self::Directory(value_as_path);
1859        }
1860        // e.g. /path/to/python
1861        if value_as_path.is_file() {
1862            return Self::File(value_as_path);
1863        }
1864
1865        // e.g. path/to/python on Windows, where path/to/python.exe is the true path
1866        #[cfg(windows)]
1867        if value_as_path.extension().is_none() {
1868            let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1869            if value_as_path.is_file() {
1870                return Self::File(value_as_path);
1871            }
1872        }
1873
1874        // During unit testing, we cannot change the working directory used by std
1875        // so we perform a check relative to the mock working directory. Ideally we'd
1876        // remove this code and use tests at the CLI level so we can change the real
1877        // directory.
1878        #[cfg(test)]
1879        if value_as_path.is_relative() {
1880            if let Ok(current_dir) = crate::current_dir() {
1881                let relative = current_dir.join(&value_as_path);
1882                if relative.is_dir() {
1883                    return Self::Directory(relative);
1884                }
1885                if relative.is_file() {
1886                    return Self::File(relative);
1887                }
1888            }
1889        }
1890        // e.g. .\path\to\python3.exe or ./path/to/python3
1891        // If it contains a path separator, we'll treat it as a full path even if it does not exist
1892        if value.contains(std::path::MAIN_SEPARATOR) {
1893            return Self::File(value_as_path);
1894        }
1895        // e.g. ./path/to/python3.exe
1896        // On Windows, Unix path separators are often valid
1897        if cfg!(windows) && value.contains('/') {
1898            return Self::File(value_as_path);
1899        }
1900        if let Ok(request) = PythonDownloadRequest::from_str(value) {
1901            return Self::Key(request);
1902        }
1903        // Finally, we'll treat it as the name of an executable (i.e. in the search PATH)
1904        // e.g. foo.exe
1905        Self::ExecutableName(value.to_string())
1906    }
1907
1908    /// Try to parse a tool name as a Python version, e.g. `uvx python311`.
1909    ///
1910    /// The `PythonRequest::parse` constructor above is intended for the `--python` flag, where the
1911    /// value is unambiguously a Python version. This alternate constructor is intended for `uvx`
1912    /// or `uvx --from`, where the executable could be either a Python version or a package name.
1913    /// There are several differences in behavior:
1914    ///
1915    /// - This only supports long names, including e.g. `pypy39` but **not** `pp39` or `39`.
1916    /// - On Windows only, this allows `pythonw` as an alias for `python`.
1917    /// - This allows `python` by itself (and on Windows, `pythonw`) as an alias for `default`.
1918    ///
1919    /// This can only return `Err` if `@` is used. Otherwise, if no match is found, it returns
1920    /// `Ok(None)`.
1921    pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
1922        let lowercase_value = &value.to_ascii_lowercase();
1923        // Omitting the empty string from these lists excludes bare versions like "39".
1924        let abstract_version_prefixes = if cfg!(windows) {
1925            &["python", "pythonw"][..]
1926        } else {
1927            &["python"][..]
1928        };
1929        // e.g. just `python`
1930        if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
1931            return Ok(Some(Self::Default));
1932        }
1933        Self::parse_versions_and_implementations(
1934            abstract_version_prefixes.iter().copied(),
1935            ImplementationName::long_names(),
1936            lowercase_value,
1937        )
1938    }
1939
1940    /// Take a value like `"python3.11"`, check whether it matches a set of abstract python
1941    /// prefixes (e.g. `"python"`, `"pythonw"`, or even `""`) or a set of specific Python
1942    /// implementations (e.g. `"cpython"` or `"pypy"`, possibly with abbreviations), and if so try
1943    /// to parse its version.
1944    ///
1945    /// This can only return `Err` if `@` is used, see
1946    /// [`try_split_prefix_and_version`][Self::try_split_prefix_and_version] below. Otherwise, if
1947    /// no match is found, it returns `Ok(None)`.
1948    fn parse_versions_and_implementations<'a>(
1949        // typically "python", possibly also "pythonw" or "" (for bare versions)
1950        abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
1951        // expected to be either long_names() or all names
1952        implementation_names: impl IntoIterator<Item = &'a str>,
1953        // the string to parse
1954        lowercase_value: &str,
1955    ) -> Result<Option<Self>, Error> {
1956        for prefix in abstract_version_prefixes {
1957            if let Some(version_request) =
1958                Self::try_split_prefix_and_version(prefix, lowercase_value)?
1959            {
1960                // e.g. `python39` or `python@39`
1961                // Note that e.g. `python` gets handled elsewhere, if at all. (It's currently
1962                // allowed in tool executables but not in --python flags.)
1963                return Ok(Some(Self::Version(version_request)));
1964            }
1965        }
1966        for implementation in implementation_names {
1967            if lowercase_value == implementation {
1968                return Ok(Some(Self::Implementation(
1969                    // e.g. `pypy`
1970                    // Safety: The name matched the possible names above
1971                    ImplementationName::from_str(implementation).unwrap(),
1972                )));
1973            }
1974            if let Some(version_request) =
1975                Self::try_split_prefix_and_version(implementation, lowercase_value)?
1976            {
1977                // e.g. `pypy39`
1978                return Ok(Some(Self::ImplementationVersion(
1979                    // Safety: The name matched the possible names above
1980                    ImplementationName::from_str(implementation).unwrap(),
1981                    version_request,
1982                )));
1983            }
1984        }
1985        Ok(None)
1986    }
1987
1988    /// Take a value like `"python3.11"`, check whether it matches a target prefix (e.g.
1989    /// `"python"`, `"pypy"`, or even `""`), and if so try to parse its version.
1990    ///
1991    /// Failing to match the prefix (e.g. `"notpython3.11"`) or failing to parse a version (e.g.
1992    /// `"python3notaversion"`) is not an error, and those cases return `Ok(None)`. The `@`
1993    /// separator is optional, and this function can only return `Err` if `@` is used. There are
1994    /// two error cases:
1995    ///
1996    /// - The value starts with `@` (e.g. `@3.11`).
1997    /// - The prefix is a match, but the version is invalid (e.g. `python@3.not.a.version`).
1998    fn try_split_prefix_and_version(
1999        prefix: &str,
2000        lowercase_value: &str,
2001    ) -> Result<Option<VersionRequest>, Error> {
2002        if lowercase_value.starts_with('@') {
2003            return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
2004        }
2005        let Some(rest) = lowercase_value.strip_prefix(prefix) else {
2006            return Ok(None);
2007        };
2008        // Just the prefix by itself (e.g. "python") is handled elsewhere.
2009        if rest.is_empty() {
2010            return Ok(None);
2011        }
2012        // The @ separator is optional. If it's present, the right half must be a version, and
2013        // parsing errors are raised to the caller.
2014        if let Some(after_at) = rest.strip_prefix('@') {
2015            if after_at == "latest" {
2016                // Handle `@latest` as a special case. It's still an error for now, but we plan to
2017                // support it. TODO(zanieb): Add `PythonRequest::Latest`
2018                return Err(Error::LatestVersionRequest);
2019            }
2020            return after_at.parse().map(Some);
2021        }
2022        // The @ was not present, so if the version fails to parse just return Ok(None). For
2023        // example, python3stuff.
2024        Ok(rest.parse().ok())
2025    }
2026
2027    /// Check if this request includes a specific patch version.
2028    pub fn includes_patch(&self) -> bool {
2029        match self {
2030            Self::Default => false,
2031            Self::Any => false,
2032            Self::Version(version_request) => version_request.patch().is_some(),
2033            Self::Directory(..) => false,
2034            Self::File(..) => false,
2035            Self::ExecutableName(..) => false,
2036            Self::Implementation(..) => false,
2037            Self::ImplementationVersion(_, version) => version.patch().is_some(),
2038            Self::Key(request) => request
2039                .version
2040                .as_ref()
2041                .is_some_and(|request| request.patch().is_some()),
2042        }
2043    }
2044
2045    /// Check if this request includes a specific prerelease version.
2046    pub fn includes_prerelease(&self) -> bool {
2047        match self {
2048            Self::Default => false,
2049            Self::Any => false,
2050            Self::Version(version_request) => version_request.prerelease().is_some(),
2051            Self::Directory(..) => false,
2052            Self::File(..) => false,
2053            Self::ExecutableName(..) => false,
2054            Self::Implementation(..) => false,
2055            Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
2056            Self::Key(request) => request
2057                .version
2058                .as_ref()
2059                .is_some_and(|request| request.prerelease().is_some()),
2060        }
2061    }
2062
2063    /// Check if a given interpreter satisfies the interpreter request.
2064    pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
2065        /// Returns `true` if the two paths refer to the same interpreter executable.
2066        fn is_same_executable(path1: &Path, path2: &Path) -> bool {
2067            path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
2068        }
2069
2070        match self {
2071            Self::Default | Self::Any => true,
2072            Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2073            Self::Directory(directory) => {
2074                // `sys.prefix` points to the environment root or `sys.executable` is the same
2075                is_same_executable(directory, interpreter.sys_prefix())
2076                    || is_same_executable(
2077                        virtualenv_python_executable(directory).as_path(),
2078                        interpreter.sys_executable(),
2079                    )
2080            }
2081            Self::File(file) => {
2082                // The interpreter satisfies the request both if it is the venv...
2083                if is_same_executable(interpreter.sys_executable(), file) {
2084                    return true;
2085                }
2086                // ...or if it is the base interpreter the venv was created from.
2087                if interpreter
2088                    .sys_base_executable()
2089                    .is_some_and(|sys_base_executable| {
2090                        is_same_executable(sys_base_executable, file)
2091                    })
2092                {
2093                    return true;
2094                }
2095                // ...or, on Windows, if both interpreters have the same base executable. On
2096                // Windows, interpreters are copied rather than symlinked, so a virtual environment
2097                // created from within a virtual environment will _not_ evaluate to the same
2098                // `sys.executable`, but will have the same `sys._base_executable`.
2099                if cfg!(windows) {
2100                    if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2101                        if let (Some(file_base), Some(interpreter_base)) = (
2102                            file_interpreter.sys_base_executable(),
2103                            interpreter.sys_base_executable(),
2104                        ) {
2105                            if is_same_executable(file_base, interpreter_base) {
2106                                return true;
2107                            }
2108                        }
2109                    }
2110                }
2111                false
2112            }
2113            Self::ExecutableName(name) => {
2114                // First, see if we have a match in the venv ...
2115                if interpreter
2116                    .sys_executable()
2117                    .file_name()
2118                    .is_some_and(|filename| filename == name.as_str())
2119                {
2120                    return true;
2121                }
2122                // ... or the venv's base interpreter (without performing IO), if that fails, ...
2123                if interpreter
2124                    .sys_base_executable()
2125                    .and_then(|executable| executable.file_name())
2126                    .is_some_and(|file_name| file_name == name.as_str())
2127                {
2128                    return true;
2129                }
2130                // ... check in `PATH`. The name we find here does not need to be the
2131                // name we install, so we can find `foopython` here which got installed as `python`.
2132                if which(name)
2133                    .ok()
2134                    .as_ref()
2135                    .and_then(|executable| executable.file_name())
2136                    .is_some_and(|file_name| file_name == name.as_str())
2137                {
2138                    return true;
2139                }
2140                false
2141            }
2142            Self::Implementation(implementation) => interpreter
2143                .implementation_name()
2144                .eq_ignore_ascii_case(implementation.into()),
2145            Self::ImplementationVersion(implementation, version) => {
2146                version.matches_interpreter(interpreter)
2147                    && interpreter
2148                        .implementation_name()
2149                        .eq_ignore_ascii_case(implementation.into())
2150            }
2151            Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2152        }
2153    }
2154
2155    /// Whether this request opts-in to a pre-release Python version.
2156    pub(crate) fn allows_prereleases(&self) -> bool {
2157        match self {
2158            Self::Default => false,
2159            Self::Any => true,
2160            Self::Version(version) => version.allows_prereleases(),
2161            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2162            Self::Implementation(_) => false,
2163            Self::ImplementationVersion(_, _) => true,
2164            Self::Key(request) => request.allows_prereleases(),
2165        }
2166    }
2167
2168    /// Whether this request opts-in to a debug Python version.
2169    fn allows_debug(&self) -> bool {
2170        match self {
2171            Self::Default => false,
2172            Self::Any => true,
2173            Self::Version(version) => version.is_debug(),
2174            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2175            Self::Implementation(_) => false,
2176            Self::ImplementationVersion(_, _) => true,
2177            Self::Key(request) => request.allows_debug(),
2178        }
2179    }
2180
2181    /// Whether this request opts-in to an alternative Python implementation, e.g., PyPy.
2182    fn allows_alternative_implementations(&self) -> bool {
2183        match self {
2184            Self::Default => false,
2185            Self::Any => true,
2186            Self::Version(_) => false,
2187            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2188            Self::Implementation(implementation)
2189            | Self::ImplementationVersion(implementation, _) => {
2190                !matches!(implementation, ImplementationName::CPython)
2191            }
2192            Self::Key(request) => request.allows_alternative_implementations(),
2193        }
2194    }
2195
2196    pub(crate) fn is_explicit_system(&self) -> bool {
2197        matches!(self, Self::File(_) | Self::Directory(_))
2198    }
2199
2200    /// Serialize the request to a canonical representation.
2201    ///
2202    /// [`Self::parse`] should always return the same request when given the output of this method.
2203    pub fn to_canonical_string(&self) -> String {
2204        match self {
2205            Self::Any => "any".to_string(),
2206            Self::Default => "default".to_string(),
2207            Self::Version(version) => version.to_string(),
2208            Self::Directory(path) => path.display().to_string(),
2209            Self::File(path) => path.display().to_string(),
2210            Self::ExecutableName(name) => name.clone(),
2211            Self::Implementation(implementation) => implementation.to_string(),
2212            Self::ImplementationVersion(implementation, version) => {
2213                format!("{implementation}@{version}")
2214            }
2215            Self::Key(request) => request.to_string(),
2216        }
2217    }
2218
2219    /// Convert an interpreter request into a concrete PEP 440 `Version` when possible.
2220    ///
2221    /// Returns `None` if the request doesn't carry an exact version
2222    pub fn as_pep440_version(&self) -> Option<Version> {
2223        match self {
2224            Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2225            Self::Key(download_request) => download_request
2226                .version()
2227                .and_then(VersionRequest::as_pep440_version),
2228            Self::Default
2229            | Self::Any
2230            | Self::Directory(_)
2231            | Self::File(_)
2232            | Self::ExecutableName(_)
2233            | Self::Implementation(_) => None,
2234        }
2235    }
2236
2237    /// Convert an interpreter request into [`VersionSpecifiers`] representing the range of
2238    /// compatible versions.
2239    ///
2240    /// Returns `None` if the request doesn't carry version constraints (e.g., a path or
2241    /// executable name).
2242    fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2243        match self {
2244            Self::Version(version) | Self::ImplementationVersion(_, version) => {
2245                version.as_version_specifiers()
2246            }
2247            Self::Key(download_request) => download_request
2248                .version()
2249                .and_then(VersionRequest::as_version_specifiers),
2250            Self::Default
2251            | Self::Any
2252            | Self::Directory(_)
2253            | Self::File(_)
2254            | Self::ExecutableName(_)
2255            | Self::Implementation(_) => None,
2256        }
2257    }
2258
2259    /// Returns `true` when this request is compatible with the given `requires-python` specifier.
2260    ///
2261    /// Requests without version constraints (e.g., paths, executable names) are always considered
2262    /// compatible. For versioned requests, compatibility means the request's version range has a
2263    /// non-empty intersection with the `requires-python` range.
2264    pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2265        let Some(specifiers) = self.as_version_specifiers() else {
2266            return true;
2267        };
2268
2269        let request_range = release_specifiers_to_ranges(specifiers);
2270        let requires_python_range =
2271            release_specifiers_to_ranges(requires_python.specifiers().clone());
2272        !request_range
2273            .intersection(&requires_python_range)
2274            .is_empty()
2275    }
2276}
2277
2278impl PythonSource {
2279    pub fn is_managed(self) -> bool {
2280        matches!(self, Self::Managed)
2281    }
2282
2283    /// Whether a pre-release Python installation from this source can be used without opt-in.
2284    fn allows_prereleases(self) -> bool {
2285        match self {
2286            Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2287            Self::SearchPath
2288            | Self::SearchPathFirst
2289            | Self::CondaPrefix
2290            | Self::BaseCondaPrefix
2291            | Self::ProvidedPath
2292            | Self::ParentInterpreter
2293            | Self::ActiveEnvironment
2294            | Self::DiscoveredEnvironment => true,
2295        }
2296    }
2297
2298    /// Whether a debug Python installation from this source can be used without opt-in.
2299    fn allows_debug(self) -> bool {
2300        match self {
2301            Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2302            Self::SearchPath
2303            | Self::SearchPathFirst
2304            | Self::CondaPrefix
2305            | Self::BaseCondaPrefix
2306            | Self::ProvidedPath
2307            | Self::ParentInterpreter
2308            | Self::ActiveEnvironment
2309            | Self::DiscoveredEnvironment => true,
2310        }
2311    }
2312
2313    /// Whether an alternative Python implementation from this source can be used without opt-in.
2314    fn allows_alternative_implementations(self) -> bool {
2315        match self {
2316            Self::Managed
2317            | Self::Registry
2318            | Self::SearchPath
2319            // TODO(zanieb): We may want to allow this at some point, but when adding this variant
2320            // we want compatibility with existing behavior
2321            | Self::SearchPathFirst
2322            | Self::MicrosoftStore => false,
2323            Self::CondaPrefix
2324            | Self::BaseCondaPrefix
2325            | Self::ProvidedPath
2326            | Self::ParentInterpreter
2327            | Self::ActiveEnvironment
2328            | Self::DiscoveredEnvironment => true,
2329        }
2330    }
2331
2332    /// Whether this source **could** be a virtual environment.
2333    ///
2334    /// This excludes the [`PythonSource::SearchPath`] although it could be in a virtual
2335    /// environment; pragmatically, that's not common and saves us from querying a bunch of system
2336    /// interpreters for no reason. It seems dubious to consider an interpreter in the `PATH` as a
2337    /// target virtual environment if it's not discovered through our virtual environment-specific
2338    /// patterns. Instead, we special case the first Python executable found on the `PATH` with
2339    /// [`PythonSource::SearchPathFirst`], allowing us to check if that's a virtual environment.
2340    /// This enables targeting the virtual environment with uv by putting its `bin/` on the `PATH`
2341    /// without setting `VIRTUAL_ENV` — but if there's another interpreter before it we will ignore
2342    /// it.
2343    fn is_maybe_virtualenv(self) -> bool {
2344        match self {
2345            Self::ProvidedPath
2346            | Self::ActiveEnvironment
2347            | Self::DiscoveredEnvironment
2348            | Self::CondaPrefix
2349            | Self::BaseCondaPrefix
2350            | Self::ParentInterpreter
2351            | Self::SearchPathFirst => true,
2352            Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2353        }
2354    }
2355
2356    /// Whether this source is "explicit", e.g., it was directly provided by the user or is
2357    /// an active virtual environment.
2358    fn is_explicit(self) -> bool {
2359        match self {
2360            Self::ProvidedPath
2361            | Self::ParentInterpreter
2362            | Self::ActiveEnvironment
2363            | Self::CondaPrefix => true,
2364            Self::Managed
2365            | Self::DiscoveredEnvironment
2366            | Self::SearchPath
2367            | Self::SearchPathFirst
2368            | Self::Registry
2369            | Self::MicrosoftStore
2370            | Self::BaseCondaPrefix => false,
2371        }
2372    }
2373
2374    /// Whether this source **could** be a system interpreter.
2375    fn is_maybe_system(self) -> bool {
2376        match self {
2377            Self::CondaPrefix
2378            | Self::BaseCondaPrefix
2379            | Self::ParentInterpreter
2380            | Self::ProvidedPath
2381            | Self::Managed
2382            | Self::SearchPath
2383            | Self::SearchPathFirst
2384            | Self::Registry
2385            | Self::MicrosoftStore => true,
2386            Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2387        }
2388    }
2389}
2390
2391impl PythonPreference {
2392    fn allows_source(self, source: PythonSource) -> bool {
2393        // If not dealing with a system interpreter source, we don't care about the preference
2394        if !matches!(
2395            source,
2396            PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2397        ) {
2398            return true;
2399        }
2400
2401        match self {
2402            Self::OnlyManaged => matches!(source, PythonSource::Managed),
2403            Self::Managed | Self::System => matches!(
2404                source,
2405                PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2406            ),
2407            Self::OnlySystem => {
2408                matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2409            }
2410        }
2411    }
2412
2413    pub(crate) fn allows_managed(self) -> bool {
2414        match self {
2415            Self::OnlySystem => false,
2416            Self::Managed | Self::System | Self::OnlyManaged => true,
2417        }
2418    }
2419
2420    /// Returns `true` if the given interpreter is allowed by this preference.
2421    ///
2422    /// Unlike [`PythonPreference::allows_source`], which checks the [`PythonSource`], this checks
2423    /// whether the interpreter's base prefix is in a managed location.
2424    fn allows_interpreter(self, interpreter: &Interpreter) -> bool {
2425        match self {
2426            Self::OnlyManaged => interpreter.is_managed(),
2427            Self::OnlySystem => !interpreter.is_managed(),
2428            Self::Managed | Self::System => true,
2429        }
2430    }
2431
2432    /// Returns `true` if the given installation is allowed by this preference.
2433    ///
2434    /// Explicit sources (e.g., provided paths, active environments) are always allowed, even if
2435    /// they conflict with the preference. We may want to invalidate the environment in some
2436    /// cases, like in projects, but we can't distinguish between explicit requests for a
2437    /// different Python preference or a persistent preference in a configuration file which
2438    /// would result in overly aggressive invalidation.
2439    pub fn allows_installation(self, installation: &PythonInstallation) -> bool {
2440        let source = installation.source;
2441        let interpreter = &installation.interpreter;
2442
2443        match self {
2444            Self::OnlyManaged => {
2445                if self.allows_interpreter(interpreter) {
2446                    true
2447                } else if source.is_explicit() {
2448                    debug!(
2449                        "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2450                        interpreter.sys_executable().display()
2451                    );
2452                    true
2453                } else {
2454                    debug!(
2455                        "Ignoring Python interpreter at `{}`: only managed interpreters allowed",
2456                        interpreter.sys_executable().display()
2457                    );
2458                    false
2459                }
2460            }
2461            // If not "only" a kind, any interpreter is okay
2462            Self::Managed | Self::System => true,
2463            Self::OnlySystem => {
2464                if self.allows_interpreter(interpreter) {
2465                    true
2466                } else if source.is_explicit() {
2467                    debug!(
2468                        "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2469                        interpreter.sys_executable().display()
2470                    );
2471                    true
2472                } else {
2473                    debug!(
2474                        "Ignoring Python interpreter at `{}`: only system interpreters allowed",
2475                        interpreter.sys_executable().display()
2476                    );
2477                    false
2478                }
2479            }
2480        }
2481    }
2482
2483    /// Returns a new preference when the `--system` flag is used.
2484    ///
2485    /// This will convert [`PythonPreference::Managed`] to [`PythonPreference::System`] when system
2486    /// is set.
2487    #[must_use]
2488    pub fn with_system_flag(self, system: bool) -> Self {
2489        match self {
2490            // TODO(zanieb): It's not clear if we want to allow `--system` to override
2491            // `--managed-python`. We should probably make this `from_system_flag` and refactor
2492            // handling of the `PythonPreference` to use an `Option` so we can tell if the user
2493            // provided it?
2494            Self::OnlyManaged => self,
2495            Self::Managed => {
2496                if system {
2497                    Self::System
2498                } else {
2499                    self
2500                }
2501            }
2502            Self::System => self,
2503            Self::OnlySystem => self,
2504        }
2505    }
2506}
2507
2508impl PythonDownloads {
2509    pub fn is_automatic(self) -> bool {
2510        matches!(self, Self::Automatic)
2511    }
2512}
2513
2514impl EnvironmentPreference {
2515    pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2516        match (system, mutable) {
2517            // When the system flag is provided, ignore virtual environments.
2518            (true, _) => Self::OnlySystem,
2519            // For mutable operations, only allow discovery of the system with explicit selection.
2520            (false, true) => Self::ExplicitSystem,
2521            // For immutable operations, we allow discovery of the system environment
2522            (false, false) => Self::Any,
2523        }
2524    }
2525
2526    /// Returns `true` if the given installation is allowed by this preference.
2527    ///
2528    /// In contrast, [`source_satisfies_environment_preference`] only checks if a
2529    /// [`PythonSource`] **could** satisfy the preference as a pre-filtering step. We cannot
2530    /// definitively know if a Python interpreter is in a virtual environment until we query it.
2531    pub(crate) fn allows_installation(self, installation: &PythonInstallation) -> bool {
2532        interpreter_satisfies_environment_preference(
2533            installation.source,
2534            &installation.interpreter,
2535            self,
2536        )
2537    }
2538}
2539
2540#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2541pub(crate) struct ExecutableName {
2542    implementation: Option<ImplementationName>,
2543    major: Option<u8>,
2544    minor: Option<u8>,
2545    patch: Option<u8>,
2546    prerelease: Option<Prerelease>,
2547    variant: PythonVariant,
2548}
2549
2550#[derive(Debug, Clone, PartialEq, Eq)]
2551struct ExecutableNameComparator<'a> {
2552    name: ExecutableName,
2553    request: &'a VersionRequest,
2554    implementation: Option<&'a ImplementationName>,
2555}
2556
2557impl Ord for ExecutableNameComparator<'_> {
2558    /// Note the comparison returns a reverse priority ordering.
2559    ///
2560    /// Higher priority items are "Greater" than lower priority items.
2561    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2562        // Prefer the default name over a specific implementation, unless an implementation was
2563        // requested
2564        let name_ordering = if self.implementation.is_some() {
2565            std::cmp::Ordering::Greater
2566        } else {
2567            std::cmp::Ordering::Less
2568        };
2569        if self.name.implementation.is_none() && other.name.implementation.is_some() {
2570            return name_ordering.reverse();
2571        }
2572        if self.name.implementation.is_some() && other.name.implementation.is_none() {
2573            return name_ordering;
2574        }
2575        // Otherwise, use the names in supported order
2576        let ordering = self.name.implementation.cmp(&other.name.implementation);
2577        if ordering != std::cmp::Ordering::Equal {
2578            return ordering;
2579        }
2580        let ordering = self.name.major.cmp(&other.name.major);
2581        let is_default_request =
2582            matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2583        if ordering != std::cmp::Ordering::Equal {
2584            return if is_default_request {
2585                ordering.reverse()
2586            } else {
2587                ordering
2588            };
2589        }
2590        let ordering = self.name.minor.cmp(&other.name.minor);
2591        if ordering != std::cmp::Ordering::Equal {
2592            return if is_default_request {
2593                ordering.reverse()
2594            } else {
2595                ordering
2596            };
2597        }
2598        let ordering = self.name.patch.cmp(&other.name.patch);
2599        if ordering != std::cmp::Ordering::Equal {
2600            return if is_default_request {
2601                ordering.reverse()
2602            } else {
2603                ordering
2604            };
2605        }
2606        let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2607        if ordering != std::cmp::Ordering::Equal {
2608            return if is_default_request {
2609                ordering.reverse()
2610            } else {
2611                ordering
2612            };
2613        }
2614        let ordering = self.name.variant.cmp(&other.name.variant);
2615        if ordering != std::cmp::Ordering::Equal {
2616            return if is_default_request {
2617                ordering.reverse()
2618            } else {
2619                ordering
2620            };
2621        }
2622        ordering
2623    }
2624}
2625
2626impl PartialOrd for ExecutableNameComparator<'_> {
2627    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2628        Some(self.cmp(other))
2629    }
2630}
2631
2632impl ExecutableName {
2633    #[must_use]
2634    fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2635        self.implementation = Some(implementation);
2636        self
2637    }
2638
2639    #[must_use]
2640    fn with_major(mut self, major: u8) -> Self {
2641        self.major = Some(major);
2642        self
2643    }
2644
2645    #[must_use]
2646    fn with_minor(mut self, minor: u8) -> Self {
2647        self.minor = Some(minor);
2648        self
2649    }
2650
2651    #[must_use]
2652    fn with_patch(mut self, patch: u8) -> Self {
2653        self.patch = Some(patch);
2654        self
2655    }
2656
2657    #[must_use]
2658    fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2659        self.prerelease = Some(prerelease);
2660        self
2661    }
2662
2663    #[must_use]
2664    fn with_variant(mut self, variant: PythonVariant) -> Self {
2665        self.variant = variant;
2666        self
2667    }
2668
2669    fn into_comparator<'a>(
2670        self,
2671        request: &'a VersionRequest,
2672        implementation: Option<&'a ImplementationName>,
2673    ) -> ExecutableNameComparator<'a> {
2674        ExecutableNameComparator {
2675            name: self,
2676            request,
2677            implementation,
2678        }
2679    }
2680}
2681
2682impl fmt::Display for ExecutableName {
2683    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2684        if let Some(implementation) = self.implementation {
2685            write!(f, "{implementation}")?;
2686        } else {
2687            f.write_str("python")?;
2688        }
2689        if let Some(major) = self.major {
2690            write!(f, "{major}")?;
2691            if let Some(minor) = self.minor {
2692                write!(f, ".{minor}")?;
2693                if let Some(patch) = self.patch {
2694                    write!(f, ".{patch}")?;
2695                }
2696            }
2697        }
2698        if let Some(prerelease) = &self.prerelease {
2699            write!(f, "{prerelease}")?;
2700        }
2701        f.write_str(self.variant.executable_suffix())?;
2702        f.write_str(EXE_SUFFIX)?;
2703        Ok(())
2704    }
2705}
2706
2707impl VersionRequest {
2708    /// Create a [`VersionRequest`] from [`VersionSpecifiers`].
2709    ///
2710    /// If the specifiers consist of a single `==` constraint, the version is parsed as a
2711    /// concrete version request (e.g., `MajorMinorPatch`) rather than a range.
2712    pub fn from_specifiers(specifiers: VersionSpecifiers, variant: PythonVariant) -> Self {
2713        if let [specifier] = specifiers.iter().as_slice() {
2714            if specifier.operator() == &uv_pep440::Operator::Equal {
2715                if let Ok(request) = Self::from_str(&specifier.version().to_string()) {
2716                    return request;
2717                }
2718            }
2719        }
2720        Self::Range(specifiers, variant)
2721    }
2722
2723    /// Drop any patch or prerelease information from the version request.
2724    #[must_use]
2725    pub fn only_minor(self) -> Self {
2726        match self {
2727            Self::Any => self,
2728            Self::Default => self,
2729            Self::Range(specifiers, variant) => Self::Range(
2730                specifiers
2731                    .into_iter()
2732                    .map(|s| s.only_minor_release())
2733                    .collect(),
2734                variant,
2735            ),
2736            Self::Major(..) => self,
2737            Self::MajorMinor(..) => self,
2738            Self::MajorMinorPatch(major, minor, _, variant)
2739            | Self::MajorMinorPrerelease(major, minor, _, variant)
2740            | Self::MajorMinorPatchPrerelease(major, minor, _, _, variant) => {
2741                Self::MajorMinor(major, minor, variant)
2742            }
2743        }
2744    }
2745
2746    /// Return possible executable names for the given version request.
2747    pub(crate) fn executable_names(
2748        &self,
2749        implementation: Option<&ImplementationName>,
2750    ) -> Vec<ExecutableName> {
2751        let prerelease = match self {
2752            Self::MajorMinorPrerelease(_, _, prerelease, _)
2753            | Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => {
2754                // Include the prerelease version, e.g., `python3.8a`
2755                Some(prerelease)
2756            }
2757            _ => None,
2758        };
2759
2760        // Push a default one
2761        let mut names = Vec::new();
2762        names.push(ExecutableName::default());
2763
2764        // Collect each variant depending on the number of versions
2765        if let Some(major) = self.major() {
2766            // e.g. `python3`
2767            names.push(ExecutableName::default().with_major(major));
2768            if let Some(minor) = self.minor() {
2769                // e.g., `python3.12`
2770                names.push(
2771                    ExecutableName::default()
2772                        .with_major(major)
2773                        .with_minor(minor),
2774                );
2775                if let Some(patch) = self.patch() {
2776                    // e.g, `python3.12.1`
2777                    names.push(
2778                        ExecutableName::default()
2779                            .with_major(major)
2780                            .with_minor(minor)
2781                            .with_patch(patch),
2782                    );
2783                }
2784            }
2785        } else {
2786            // Include `3` by default, e.g., `python3`
2787            names.push(ExecutableName::default().with_major(3));
2788        }
2789
2790        if let Some(prerelease) = prerelease {
2791            // Include the prerelease version, e.g., `python3.8a`
2792            for i in 0..names.len() {
2793                let name = names[i];
2794                if name.minor.is_none() {
2795                    // We don't want to include the pre-release marker here
2796                    // e.g. `pythonrc1` and `python3rc1` don't make sense
2797                    continue;
2798                }
2799                names.push(name.with_prerelease(*prerelease));
2800            }
2801        }
2802
2803        // Add all the implementation-specific names
2804        if let Some(implementation) = implementation {
2805            for i in 0..names.len() {
2806                let name = names[i].with_implementation(*implementation);
2807                names.push(name);
2808            }
2809        } else {
2810            // When looking for all implementations, include all possible names
2811            if matches!(self, Self::Any) {
2812                for i in 0..names.len() {
2813                    for implementation in ImplementationName::iter_all() {
2814                        let name = names[i].with_implementation(implementation);
2815                        names.push(name);
2816                    }
2817                }
2818            }
2819        }
2820
2821        // Include free-threaded variants
2822        if let Some(variant) = self.variant() {
2823            if variant != PythonVariant::Default {
2824                for i in 0..names.len() {
2825                    let name = names[i].with_variant(variant);
2826                    names.push(name);
2827                }
2828            }
2829        }
2830
2831        names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
2832        names.reverse();
2833
2834        names
2835    }
2836
2837    /// Return the major version segment of the request, if any.
2838    fn major(&self) -> Option<u8> {
2839        match self {
2840            Self::Any | Self::Default | Self::Range(_, _) => None,
2841            Self::Major(major, _) => Some(*major),
2842            Self::MajorMinor(major, _, _) => Some(*major),
2843            Self::MajorMinorPatch(major, _, _, _) => Some(*major),
2844            Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2845            Self::MajorMinorPatchPrerelease(major, _, _, _, _) => Some(*major),
2846        }
2847    }
2848
2849    /// Return the minor version segment of the request, if any.
2850    fn minor(&self) -> Option<u8> {
2851        match self {
2852            Self::Any | Self::Default | Self::Range(_, _) => None,
2853            Self::Major(_, _) => None,
2854            Self::MajorMinor(_, minor, _) => Some(*minor),
2855            Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
2856            Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2857            Self::MajorMinorPatchPrerelease(_, minor, _, _, _) => Some(*minor),
2858        }
2859    }
2860
2861    /// Return the patch version segment of the request, if any.
2862    fn patch(&self) -> Option<u8> {
2863        match self {
2864            Self::Any | Self::Default | Self::Range(_, _) => None,
2865            Self::Major(_, _) => None,
2866            Self::MajorMinor(_, _, _) => None,
2867            Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
2868            Self::MajorMinorPrerelease(_, _, _, _) => None,
2869            Self::MajorMinorPatchPrerelease(_, _, patch, _, _) => Some(*patch),
2870        }
2871    }
2872
2873    /// Return the pre-release segment of the request, if any.
2874    fn prerelease(&self) -> Option<&Prerelease> {
2875        match self {
2876            Self::Any | Self::Default | Self::Range(_, _) => None,
2877            Self::Major(_, _) => None,
2878            Self::MajorMinor(_, _, _) => None,
2879            Self::MajorMinorPatch(_, _, _, _) => None,
2880            Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
2881            Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => Some(prerelease),
2882        }
2883    }
2884
2885    /// Check if the request is for a version supported by uv.
2886    ///
2887    /// If not, an `Err` is returned with an explanatory message.
2888    fn check_supported(&self) -> Result<(), String> {
2889        match self {
2890            Self::Any | Self::Default => (),
2891            Self::Major(major, _) => {
2892                if *major < 3 {
2893                    return Err(format!(
2894                        "Python <3 is not supported but {major} was requested."
2895                    ));
2896                }
2897            }
2898            Self::MajorMinor(major, minor, _) => {
2899                if (*major, *minor) < (3, 6) {
2900                    return Err(format!(
2901                        "Python <3.6 is not supported but {major}.{minor} was requested."
2902                    ));
2903                }
2904            }
2905            Self::MajorMinorPatch(major, minor, patch, _) => {
2906                if (*major, *minor) < (3, 6) {
2907                    return Err(format!(
2908                        "Python <3.6 is not supported but {major}.{minor}.{patch} was requested."
2909                    ));
2910                }
2911            }
2912            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2913                if (*major, *minor) < (3, 6) {
2914                    return Err(format!(
2915                        "Python <3.6 is not supported but {major}.{minor}{prerelease} was requested."
2916                    ));
2917                }
2918            }
2919            Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
2920                if (*major, *minor) < (3, 6) {
2921                    return Err(format!(
2922                        "Python <3.6 is not supported but {major}.{minor}.{patch}{prerelease} was requested."
2923                    ));
2924                }
2925            }
2926            // TODO(zanieb): We could do some checking here to see if the range can be satisfied
2927            Self::Range(_, _) => (),
2928        }
2929
2930        if self.is_freethreaded() {
2931            if let Self::MajorMinor(major, minor, _) = self.clone().without_patch() {
2932                if (major, minor) < (3, 13) {
2933                    return Err(format!(
2934                        "Python <3.13 does not support free-threading but {self} was requested."
2935                    ));
2936                }
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}