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                && let Err(err) = version.check_supported()
1221            {
1222                return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
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                && let Err(err) = result
1297            {
1298                first_error = Some(err);
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 = ImplementationName::iter_all().flat_map(|implementation| {
1844            std::iter::once(implementation.long_name()).chain(implementation.short_name())
1845        });
1846        // Abstract versions like `python@312`, `python312`, or `312`, plus implementations and
1847        // implementation versions like `pypy`, `pypy@312` or `pypy312`.
1848        if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1849            abstract_version_prefixes,
1850            all_implementation_names,
1851            lowercase_value,
1852        ) {
1853            return request;
1854        }
1855
1856        let value_as_path = PathBuf::from(value);
1857        // e.g. /path/to/.venv
1858        if value_as_path.is_dir() {
1859            return Self::Directory(value_as_path);
1860        }
1861        // e.g. /path/to/python
1862        if value_as_path.is_file() {
1863            return Self::File(value_as_path);
1864        }
1865
1866        // e.g. path/to/python on Windows, where path/to/python.exe is the true path
1867        #[cfg(windows)]
1868        if value_as_path.extension().is_none() {
1869            let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1870            if value_as_path.is_file() {
1871                return Self::File(value_as_path);
1872            }
1873        }
1874
1875        // During unit testing, we cannot change the working directory used by std
1876        // so we perform a check relative to the mock working directory. Ideally we'd
1877        // remove this code and use tests at the CLI level so we can change the real
1878        // directory.
1879        #[cfg(test)]
1880        if value_as_path.is_relative() {
1881            if let Ok(current_dir) = crate::current_dir() {
1882                let relative = current_dir.join(&value_as_path);
1883                if relative.is_dir() {
1884                    return Self::Directory(relative);
1885                }
1886                if relative.is_file() {
1887                    return Self::File(relative);
1888                }
1889            }
1890        }
1891        // e.g. .\path\to\python3.exe or ./path/to/python3
1892        // If it contains a path separator, we'll treat it as a full path even if it does not exist
1893        if value.contains(std::path::MAIN_SEPARATOR) {
1894            return Self::File(value_as_path);
1895        }
1896        // e.g. ./path/to/python3.exe
1897        // On Windows, Unix path separators are often valid
1898        if cfg!(windows) && value.contains('/') {
1899            return Self::File(value_as_path);
1900        }
1901        if let Ok(request) = PythonDownloadRequest::from_str(value) {
1902            return Self::Key(request);
1903        }
1904        // Finally, we'll treat it as the name of an executable (i.e. in the search PATH)
1905        // e.g. foo.exe
1906        Self::ExecutableName(value.to_string())
1907    }
1908
1909    /// Try to parse a tool name as a Python version, e.g. `uvx python311`.
1910    ///
1911    /// The `PythonRequest::parse` constructor above is intended for the `--python` flag, where the
1912    /// value is unambiguously a Python version. This alternate constructor is intended for `uvx`
1913    /// or `uvx --from`, where the executable could be either a Python version or a package name.
1914    /// There are several differences in behavior:
1915    ///
1916    /// - This only supports long names, including e.g. `pypy39` but **not** `pp39` or `39`.
1917    /// - On Windows only, this allows `pythonw` as an alias for `python`.
1918    /// - This allows `python` by itself (and on Windows, `pythonw`) as an alias for `default`.
1919    ///
1920    /// This can only return `Err` if `@` is used. Otherwise, if no match is found, it returns
1921    /// `Ok(None)`.
1922    pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
1923        let lowercase_value = &value.to_ascii_lowercase();
1924        // Omitting the empty string from these lists excludes bare versions like "39".
1925        let abstract_version_prefixes = if cfg!(windows) {
1926            &["python", "pythonw"][..]
1927        } else {
1928            &["python"][..]
1929        };
1930        // e.g. just `python`
1931        if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
1932            return Ok(Some(Self::Default));
1933        }
1934        Self::parse_versions_and_implementations(
1935            abstract_version_prefixes.iter().copied(),
1936            ImplementationName::iter_all().map(ImplementationName::long_name),
1937            lowercase_value,
1938        )
1939    }
1940
1941    /// Take a value like `"python3.11"`, check whether it matches a set of abstract python
1942    /// prefixes (e.g. `"python"`, `"pythonw"`, or even `""`) or a set of specific Python
1943    /// implementations (e.g. `"cpython"` or `"pypy"`, possibly with abbreviations), and if so try
1944    /// to parse its version.
1945    ///
1946    /// This can only return `Err` if `@` is used, see
1947    /// [`try_split_prefix_and_version`][Self::try_split_prefix_and_version] below. Otherwise, if
1948    /// no match is found, it returns `Ok(None)`.
1949    fn parse_versions_and_implementations<'a>(
1950        // typically "python", possibly also "pythonw" or "" (for bare versions)
1951        abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
1952        // expected to be either long names or all names
1953        implementation_names: impl IntoIterator<Item = &'a str>,
1954        // the string to parse
1955        lowercase_value: &str,
1956    ) -> Result<Option<Self>, Error> {
1957        for prefix in abstract_version_prefixes {
1958            if let Some(version_request) =
1959                Self::try_split_prefix_and_version(prefix, lowercase_value)?
1960            {
1961                // e.g. `python39` or `python@39`
1962                // Note that e.g. `python` gets handled elsewhere, if at all. (It's currently
1963                // allowed in tool executables but not in --python flags.)
1964                return Ok(Some(Self::Version(version_request)));
1965            }
1966        }
1967        for implementation in implementation_names {
1968            if lowercase_value == implementation {
1969                return Ok(Some(Self::Implementation(
1970                    // e.g. `pypy`
1971                    // Safety: The name matched the possible names above
1972                    ImplementationName::from_str(implementation).unwrap(),
1973                )));
1974            }
1975            if let Some(version_request) =
1976                Self::try_split_prefix_and_version(implementation, lowercase_value)?
1977            {
1978                // e.g. `pypy39`
1979                return Ok(Some(Self::ImplementationVersion(
1980                    // Safety: The name matched the possible names above
1981                    ImplementationName::from_str(implementation).unwrap(),
1982                    version_request,
1983                )));
1984            }
1985        }
1986        Ok(None)
1987    }
1988
1989    /// Take a value like `"python3.11"`, check whether it matches a target prefix (e.g.
1990    /// `"python"`, `"pypy"`, or even `""`), and if so try to parse its version.
1991    ///
1992    /// Failing to match the prefix (e.g. `"notpython3.11"`) or failing to parse a version (e.g.
1993    /// `"python3notaversion"`) is not an error, and those cases return `Ok(None)`. The `@`
1994    /// separator is optional, and this function can only return `Err` if `@` is used. There are
1995    /// two error cases:
1996    ///
1997    /// - The value starts with `@` (e.g. `@3.11`).
1998    /// - The prefix is a match, but the version is invalid (e.g. `python@3.not.a.version`).
1999    fn try_split_prefix_and_version(
2000        prefix: &str,
2001        lowercase_value: &str,
2002    ) -> Result<Option<VersionRequest>, Error> {
2003        if lowercase_value.starts_with('@') {
2004            return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
2005        }
2006        let Some(rest) = lowercase_value.strip_prefix(prefix) else {
2007            return Ok(None);
2008        };
2009        // Just the prefix by itself (e.g. "python") is handled elsewhere.
2010        if rest.is_empty() {
2011            return Ok(None);
2012        }
2013        // The @ separator is optional. If it's present, the right half must be a version, and
2014        // parsing errors are raised to the caller.
2015        if let Some(after_at) = rest.strip_prefix('@') {
2016            if after_at == "latest" {
2017                // Handle `@latest` as a special case. It's still an error for now, but we plan to
2018                // support it. TODO(zanieb): Add `PythonRequest::Latest`
2019                return Err(Error::LatestVersionRequest);
2020            }
2021            return after_at.parse().map(Some);
2022        }
2023        // The @ was not present, so if the version fails to parse just return Ok(None). For
2024        // example, python3stuff.
2025        Ok(rest.parse().ok())
2026    }
2027
2028    /// Check if this request includes a specific patch version.
2029    pub fn includes_patch(&self) -> bool {
2030        match self {
2031            Self::Default => false,
2032            Self::Any => false,
2033            Self::Version(version_request) => version_request.patch().is_some(),
2034            Self::Directory(..) => false,
2035            Self::File(..) => false,
2036            Self::ExecutableName(..) => false,
2037            Self::Implementation(..) => false,
2038            Self::ImplementationVersion(_, version) => version.patch().is_some(),
2039            Self::Key(request) => request
2040                .version
2041                .as_ref()
2042                .is_some_and(|request| request.patch().is_some()),
2043        }
2044    }
2045
2046    /// Check if this request includes a specific prerelease version.
2047    pub fn includes_prerelease(&self) -> bool {
2048        match self {
2049            Self::Default => false,
2050            Self::Any => false,
2051            Self::Version(version_request) => version_request.prerelease().is_some(),
2052            Self::Directory(..) => false,
2053            Self::File(..) => false,
2054            Self::ExecutableName(..) => false,
2055            Self::Implementation(..) => false,
2056            Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
2057            Self::Key(request) => request
2058                .version
2059                .as_ref()
2060                .is_some_and(|request| request.prerelease().is_some()),
2061        }
2062    }
2063
2064    /// Check if a given interpreter satisfies the interpreter request.
2065    pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
2066        /// Returns `true` if the two paths refer to the same interpreter executable.
2067        fn is_same_executable(path1: &Path, path2: &Path) -> bool {
2068            path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
2069        }
2070
2071        match self {
2072            Self::Default | Self::Any => true,
2073            Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2074            Self::Directory(directory) => {
2075                // `sys.prefix` points to the environment root or `sys.executable` is the same
2076                is_same_executable(directory, interpreter.sys_prefix())
2077                    || is_same_executable(
2078                        virtualenv_python_executable(directory).as_path(),
2079                        interpreter.sys_executable(),
2080                    )
2081            }
2082            Self::File(file) => {
2083                // The interpreter satisfies the request both if it is the venv...
2084                if is_same_executable(interpreter.sys_executable(), file) {
2085                    return true;
2086                }
2087                // ...or if it is the base interpreter the venv was created from.
2088                if interpreter
2089                    .sys_base_executable()
2090                    .is_some_and(|sys_base_executable| {
2091                        is_same_executable(sys_base_executable, file)
2092                    })
2093                {
2094                    return true;
2095                }
2096                // ...or, on Windows, if both interpreters have the same base executable. On
2097                // Windows, interpreters are copied rather than symlinked, so a virtual environment
2098                // created from within a virtual environment will _not_ evaluate to the same
2099                // `sys.executable`, but will have the same `sys._base_executable`.
2100                if cfg!(windows) {
2101                    if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2102                        if let (Some(file_base), Some(interpreter_base)) = (
2103                            file_interpreter.sys_base_executable(),
2104                            interpreter.sys_base_executable(),
2105                        ) {
2106                            if is_same_executable(file_base, interpreter_base) {
2107                                return true;
2108                            }
2109                        }
2110                    }
2111                }
2112                false
2113            }
2114            Self::ExecutableName(name) => {
2115                // First, see if we have a match in the venv ...
2116                if interpreter
2117                    .sys_executable()
2118                    .file_name()
2119                    .is_some_and(|filename| filename == name.as_str())
2120                {
2121                    return true;
2122                }
2123                // ... or the venv's base interpreter (without performing IO), if that fails, ...
2124                if interpreter
2125                    .sys_base_executable()
2126                    .and_then(|executable| executable.file_name())
2127                    .is_some_and(|file_name| file_name == name.as_str())
2128                {
2129                    return true;
2130                }
2131                // ... check in `PATH`. The name we find here does not need to be the
2132                // name we install, so we can find `foopython` here which got installed as `python`.
2133                if which(name)
2134                    .ok()
2135                    .as_ref()
2136                    .and_then(|executable| executable.file_name())
2137                    .is_some_and(|file_name| file_name == name.as_str())
2138                {
2139                    return true;
2140                }
2141                false
2142            }
2143            Self::Implementation(implementation) => interpreter
2144                .implementation_name()
2145                .eq_ignore_ascii_case(implementation.long_name()),
2146            Self::ImplementationVersion(implementation, version) => {
2147                version.matches_interpreter(interpreter)
2148                    && interpreter
2149                        .implementation_name()
2150                        .eq_ignore_ascii_case(implementation.long_name())
2151            }
2152            Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2153        }
2154    }
2155
2156    /// Whether this request opts-in to a pre-release Python version.
2157    pub(crate) fn allows_prereleases(&self) -> bool {
2158        match self {
2159            Self::Default => false,
2160            Self::Any => true,
2161            Self::Version(version) => version.allows_prereleases(),
2162            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2163            Self::Implementation(_) => false,
2164            Self::ImplementationVersion(_, _) => true,
2165            Self::Key(request) => request.allows_prereleases(),
2166        }
2167    }
2168
2169    /// Whether this request opts-in to a debug Python version.
2170    fn allows_debug(&self) -> bool {
2171        match self {
2172            Self::Default => false,
2173            Self::Any => true,
2174            Self::Version(version) => version.is_debug(),
2175            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2176            Self::Implementation(_) => false,
2177            Self::ImplementationVersion(_, _) => true,
2178            Self::Key(request) => request.allows_debug(),
2179        }
2180    }
2181
2182    /// Whether this request opts-in to an alternative Python implementation, e.g., PyPy.
2183    fn allows_alternative_implementations(&self) -> bool {
2184        match self {
2185            Self::Default => false,
2186            Self::Any => true,
2187            Self::Version(_) => false,
2188            Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2189            Self::Implementation(implementation)
2190            | Self::ImplementationVersion(implementation, _) => {
2191                !matches!(implementation, ImplementationName::CPython)
2192            }
2193            Self::Key(request) => request.allows_alternative_implementations(),
2194        }
2195    }
2196
2197    pub(crate) fn is_explicit_system(&self) -> bool {
2198        matches!(self, Self::File(_) | Self::Directory(_))
2199    }
2200
2201    /// Serialize the request to a canonical representation.
2202    ///
2203    /// [`Self::parse`] should always return the same request when given the output of this method.
2204    pub fn to_canonical_string(&self) -> String {
2205        match self {
2206            Self::Any => "any".to_string(),
2207            Self::Default => "default".to_string(),
2208            Self::Version(version) => version.to_string(),
2209            Self::Directory(path) => path.display().to_string(),
2210            Self::File(path) => path.display().to_string(),
2211            Self::ExecutableName(name) => name.clone(),
2212            Self::Implementation(implementation) => implementation.to_string(),
2213            Self::ImplementationVersion(implementation, version) => {
2214                format!("{implementation}@{version}")
2215            }
2216            Self::Key(request) => request.to_string(),
2217        }
2218    }
2219
2220    /// Convert an interpreter request into a concrete PEP 440 `Version` when possible.
2221    ///
2222    /// Returns `None` if the request doesn't carry an exact version
2223    pub fn as_pep440_version(&self) -> Option<Version> {
2224        match self {
2225            Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2226            Self::Key(download_request) => download_request
2227                .version()
2228                .and_then(VersionRequest::as_pep440_version),
2229            Self::Default
2230            | Self::Any
2231            | Self::Directory(_)
2232            | Self::File(_)
2233            | Self::ExecutableName(_)
2234            | Self::Implementation(_) => None,
2235        }
2236    }
2237
2238    /// Convert an interpreter request into [`VersionSpecifiers`] representing the range of
2239    /// compatible versions.
2240    ///
2241    /// Returns `None` if the request doesn't carry version constraints (e.g., a path or
2242    /// executable name).
2243    fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2244        match self {
2245            Self::Version(version) | Self::ImplementationVersion(_, version) => {
2246                version.as_version_specifiers()
2247            }
2248            Self::Key(download_request) => download_request
2249                .version()
2250                .and_then(VersionRequest::as_version_specifiers),
2251            Self::Default
2252            | Self::Any
2253            | Self::Directory(_)
2254            | Self::File(_)
2255            | Self::ExecutableName(_)
2256            | Self::Implementation(_) => None,
2257        }
2258    }
2259
2260    /// Returns `true` when this request is compatible with the given `requires-python` specifier.
2261    ///
2262    /// Requests without version constraints (e.g., paths, executable names) are always considered
2263    /// compatible. For versioned requests, compatibility means the request's version range has a
2264    /// non-empty intersection with the `requires-python` range.
2265    pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2266        let Some(specifiers) = self.as_version_specifiers() else {
2267            return true;
2268        };
2269
2270        let request_range = release_specifiers_to_ranges(specifiers);
2271        let requires_python_range =
2272            release_specifiers_to_ranges(requires_python.specifiers().clone());
2273        !request_range
2274            .intersection(&requires_python_range)
2275            .is_empty()
2276    }
2277}
2278
2279impl PythonSource {
2280    pub fn is_managed(self) -> bool {
2281        matches!(self, Self::Managed)
2282    }
2283
2284    /// Whether a pre-release Python installation from this source can be used without opt-in.
2285    fn allows_prereleases(self) -> bool {
2286        match self {
2287            Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2288            Self::SearchPath
2289            | Self::SearchPathFirst
2290            | Self::CondaPrefix
2291            | Self::BaseCondaPrefix
2292            | Self::ProvidedPath
2293            | Self::ParentInterpreter
2294            | Self::ActiveEnvironment
2295            | Self::DiscoveredEnvironment => true,
2296        }
2297    }
2298
2299    /// Whether a debug Python installation from this source can be used without opt-in.
2300    fn allows_debug(self) -> bool {
2301        match self {
2302            Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2303            Self::SearchPath
2304            | Self::SearchPathFirst
2305            | Self::CondaPrefix
2306            | Self::BaseCondaPrefix
2307            | Self::ProvidedPath
2308            | Self::ParentInterpreter
2309            | Self::ActiveEnvironment
2310            | Self::DiscoveredEnvironment => true,
2311        }
2312    }
2313
2314    /// Whether an alternative Python implementation from this source can be used without opt-in.
2315    fn allows_alternative_implementations(self) -> bool {
2316        match self {
2317            Self::Managed
2318            | Self::Registry
2319            | Self::SearchPath
2320            // TODO(zanieb): We may want to allow this at some point, but when adding this variant
2321            // we want compatibility with existing behavior
2322            | Self::SearchPathFirst
2323            | Self::MicrosoftStore => false,
2324            Self::CondaPrefix
2325            | Self::BaseCondaPrefix
2326            | Self::ProvidedPath
2327            | Self::ParentInterpreter
2328            | Self::ActiveEnvironment
2329            | Self::DiscoveredEnvironment => true,
2330        }
2331    }
2332
2333    /// Whether this source **could** be a virtual environment.
2334    ///
2335    /// This excludes the [`PythonSource::SearchPath`] although it could be in a virtual
2336    /// environment; pragmatically, that's not common and saves us from querying a bunch of system
2337    /// interpreters for no reason. It seems dubious to consider an interpreter in the `PATH` as a
2338    /// target virtual environment if it's not discovered through our virtual environment-specific
2339    /// patterns. Instead, we special case the first Python executable found on the `PATH` with
2340    /// [`PythonSource::SearchPathFirst`], allowing us to check if that's a virtual environment.
2341    /// This enables targeting the virtual environment with uv by putting its `bin/` on the `PATH`
2342    /// without setting `VIRTUAL_ENV` — but if there's another interpreter before it we will ignore
2343    /// it.
2344    fn is_maybe_virtualenv(self) -> bool {
2345        match self {
2346            Self::ProvidedPath
2347            | Self::ActiveEnvironment
2348            | Self::DiscoveredEnvironment
2349            | Self::CondaPrefix
2350            | Self::BaseCondaPrefix
2351            | Self::ParentInterpreter
2352            | Self::SearchPathFirst => true,
2353            Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2354        }
2355    }
2356
2357    /// Whether this source is "explicit", e.g., it was directly provided by the user or is
2358    /// an active virtual environment.
2359    fn is_explicit(self) -> bool {
2360        match self {
2361            Self::ProvidedPath
2362            | Self::ParentInterpreter
2363            | Self::ActiveEnvironment
2364            | Self::CondaPrefix => true,
2365            Self::Managed
2366            | Self::DiscoveredEnvironment
2367            | Self::SearchPath
2368            | Self::SearchPathFirst
2369            | Self::Registry
2370            | Self::MicrosoftStore
2371            | Self::BaseCondaPrefix => false,
2372        }
2373    }
2374
2375    /// Whether this source **could** be a system interpreter.
2376    fn is_maybe_system(self) -> bool {
2377        match self {
2378            Self::CondaPrefix
2379            | Self::BaseCondaPrefix
2380            | Self::ParentInterpreter
2381            | Self::ProvidedPath
2382            | Self::Managed
2383            | Self::SearchPath
2384            | Self::SearchPathFirst
2385            | Self::Registry
2386            | Self::MicrosoftStore => true,
2387            Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2388        }
2389    }
2390}
2391
2392impl PythonPreference {
2393    fn allows_source(self, source: PythonSource) -> bool {
2394        // If not dealing with a system interpreter source, we don't care about the preference
2395        if !matches!(
2396            source,
2397            PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2398        ) {
2399            return true;
2400        }
2401
2402        match self {
2403            Self::OnlyManaged => matches!(source, PythonSource::Managed),
2404            Self::Managed | Self::System => matches!(
2405                source,
2406                PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2407            ),
2408            Self::OnlySystem => {
2409                matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2410            }
2411        }
2412    }
2413
2414    pub(crate) fn allows_managed(self) -> bool {
2415        match self {
2416            Self::OnlySystem => false,
2417            Self::Managed | Self::System | Self::OnlyManaged => true,
2418        }
2419    }
2420
2421    /// Returns `true` if the given interpreter is allowed by this preference.
2422    ///
2423    /// Unlike [`PythonPreference::allows_source`], which checks the [`PythonSource`], this checks
2424    /// whether the interpreter's base prefix is in a managed location.
2425    fn allows_interpreter(self, interpreter: &Interpreter) -> bool {
2426        match self {
2427            Self::OnlyManaged => interpreter.is_managed(),
2428            Self::OnlySystem => !interpreter.is_managed(),
2429            Self::Managed | Self::System => true,
2430        }
2431    }
2432
2433    /// Returns `true` if the given installation is allowed by this preference.
2434    ///
2435    /// Explicit sources (e.g., provided paths, active environments) are always allowed, even if
2436    /// they conflict with the preference. We may want to invalidate the environment in some
2437    /// cases, like in projects, but we can't distinguish between explicit requests for a
2438    /// different Python preference or a persistent preference in a configuration file which
2439    /// would result in overly aggressive invalidation.
2440    pub fn allows_installation(self, installation: &PythonInstallation) -> bool {
2441        let source = installation.source;
2442        let interpreter = &installation.interpreter;
2443
2444        match self {
2445            Self::OnlyManaged => {
2446                if self.allows_interpreter(interpreter) {
2447                    true
2448                } else if source.is_explicit() {
2449                    debug!(
2450                        "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2451                        interpreter.sys_executable().display()
2452                    );
2453                    true
2454                } else {
2455                    debug!(
2456                        "Ignoring Python interpreter at `{}`: only managed interpreters allowed",
2457                        interpreter.sys_executable().display()
2458                    );
2459                    false
2460                }
2461            }
2462            // If not "only" a kind, any interpreter is okay
2463            Self::Managed | Self::System => true,
2464            Self::OnlySystem => {
2465                if self.allows_interpreter(interpreter) {
2466                    true
2467                } else if source.is_explicit() {
2468                    debug!(
2469                        "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2470                        interpreter.sys_executable().display()
2471                    );
2472                    true
2473                } else {
2474                    debug!(
2475                        "Ignoring Python interpreter at `{}`: only system interpreters allowed",
2476                        interpreter.sys_executable().display()
2477                    );
2478                    false
2479                }
2480            }
2481        }
2482    }
2483
2484    /// Returns a new preference when the `--system` flag is used.
2485    ///
2486    /// This will convert [`PythonPreference::Managed`] to [`PythonPreference::System`] when system
2487    /// is set.
2488    #[must_use]
2489    pub fn with_system_flag(self, system: bool) -> Self {
2490        match self {
2491            // TODO(zanieb): It's not clear if we want to allow `--system` to override
2492            // `--managed-python`. We should probably make this `from_system_flag` and refactor
2493            // handling of the `PythonPreference` to use an `Option` so we can tell if the user
2494            // provided it?
2495            Self::OnlyManaged => self,
2496            Self::Managed => {
2497                if system {
2498                    Self::System
2499                } else {
2500                    self
2501                }
2502            }
2503            Self::System => self,
2504            Self::OnlySystem => self,
2505        }
2506    }
2507}
2508
2509impl PythonDownloads {
2510    pub fn is_automatic(self) -> bool {
2511        matches!(self, Self::Automatic)
2512    }
2513}
2514
2515impl EnvironmentPreference {
2516    pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2517        match (system, mutable) {
2518            // When the system flag is provided, ignore virtual environments.
2519            (true, _) => Self::OnlySystem,
2520            // For mutable operations, only allow discovery of the system with explicit selection.
2521            (false, true) => Self::ExplicitSystem,
2522            // For immutable operations, we allow discovery of the system environment
2523            (false, false) => Self::Any,
2524        }
2525    }
2526
2527    /// Returns `true` if the given installation is allowed by this preference.
2528    ///
2529    /// In contrast, [`source_satisfies_environment_preference`] only checks if a
2530    /// [`PythonSource`] **could** satisfy the preference as a pre-filtering step. We cannot
2531    /// definitively know if a Python interpreter is in a virtual environment until we query it.
2532    pub(crate) fn allows_installation(self, installation: &PythonInstallation) -> bool {
2533        interpreter_satisfies_environment_preference(
2534            installation.source,
2535            &installation.interpreter,
2536            self,
2537        )
2538    }
2539}
2540
2541#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2542pub(crate) struct ExecutableName {
2543    implementation: Option<ImplementationName>,
2544    major: Option<u8>,
2545    minor: Option<u8>,
2546    patch: Option<u8>,
2547    prerelease: Option<Prerelease>,
2548    variant: PythonVariant,
2549}
2550
2551#[derive(Debug, Clone, PartialEq, Eq)]
2552struct ExecutableNameComparator<'a> {
2553    name: ExecutableName,
2554    request: &'a VersionRequest,
2555    implementation: Option<&'a ImplementationName>,
2556}
2557
2558impl Ord for ExecutableNameComparator<'_> {
2559    /// Note the comparison returns a reverse priority ordering.
2560    ///
2561    /// Higher priority items are "Greater" than lower priority items.
2562    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2563        // Prefer the default name over a specific implementation, unless an implementation was
2564        // requested
2565        let name_ordering = if self.implementation.is_some() {
2566            std::cmp::Ordering::Greater
2567        } else {
2568            std::cmp::Ordering::Less
2569        };
2570        if self.name.implementation.is_none() && other.name.implementation.is_some() {
2571            return name_ordering.reverse();
2572        }
2573        if self.name.implementation.is_some() && other.name.implementation.is_none() {
2574            return name_ordering;
2575        }
2576        // Otherwise, use the names in supported order
2577        let ordering = self.name.implementation.cmp(&other.name.implementation);
2578        if ordering != std::cmp::Ordering::Equal {
2579            return ordering;
2580        }
2581        let ordering = self.name.major.cmp(&other.name.major);
2582        let is_default_request =
2583            matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2584        if ordering != std::cmp::Ordering::Equal {
2585            return if is_default_request {
2586                ordering.reverse()
2587            } else {
2588                ordering
2589            };
2590        }
2591        let ordering = self.name.minor.cmp(&other.name.minor);
2592        if ordering != std::cmp::Ordering::Equal {
2593            return if is_default_request {
2594                ordering.reverse()
2595            } else {
2596                ordering
2597            };
2598        }
2599        let ordering = self.name.patch.cmp(&other.name.patch);
2600        if ordering != std::cmp::Ordering::Equal {
2601            return if is_default_request {
2602                ordering.reverse()
2603            } else {
2604                ordering
2605            };
2606        }
2607        let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2608        if ordering != std::cmp::Ordering::Equal {
2609            return if is_default_request {
2610                ordering.reverse()
2611            } else {
2612                ordering
2613            };
2614        }
2615        let ordering = self.name.variant.cmp(&other.name.variant);
2616        if ordering != std::cmp::Ordering::Equal {
2617            return if is_default_request {
2618                ordering.reverse()
2619            } else {
2620                ordering
2621            };
2622        }
2623        ordering
2624    }
2625}
2626
2627impl PartialOrd for ExecutableNameComparator<'_> {
2628    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2629        Some(self.cmp(other))
2630    }
2631}
2632
2633impl ExecutableName {
2634    #[must_use]
2635    fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2636        self.implementation = Some(implementation);
2637        self
2638    }
2639
2640    #[must_use]
2641    fn with_major(mut self, major: u8) -> Self {
2642        self.major = Some(major);
2643        self
2644    }
2645
2646    #[must_use]
2647    fn with_minor(mut self, minor: u8) -> Self {
2648        self.minor = Some(minor);
2649        self
2650    }
2651
2652    #[must_use]
2653    fn with_patch(mut self, patch: u8) -> Self {
2654        self.patch = Some(patch);
2655        self
2656    }
2657
2658    #[must_use]
2659    fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2660        self.prerelease = Some(prerelease);
2661        self
2662    }
2663
2664    #[must_use]
2665    fn with_variant(mut self, variant: PythonVariant) -> Self {
2666        self.variant = variant;
2667        self
2668    }
2669
2670    fn into_comparator<'a>(
2671        self,
2672        request: &'a VersionRequest,
2673        implementation: Option<&'a ImplementationName>,
2674    ) -> ExecutableNameComparator<'a> {
2675        ExecutableNameComparator {
2676            name: self,
2677            request,
2678            implementation,
2679        }
2680    }
2681}
2682
2683impl fmt::Display for ExecutableName {
2684    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2685        if let Some(implementation) = self.implementation {
2686            write!(f, "{implementation}")?;
2687        } else {
2688            f.write_str("python")?;
2689        }
2690        if let Some(major) = self.major {
2691            write!(f, "{major}")?;
2692            if let Some(minor) = self.minor {
2693                write!(f, ".{minor}")?;
2694                if let Some(patch) = self.patch {
2695                    write!(f, ".{patch}")?;
2696                }
2697            }
2698        }
2699        if let Some(prerelease) = &self.prerelease {
2700            write!(f, "{prerelease}")?;
2701        }
2702        f.write_str(self.variant.executable_suffix())?;
2703        f.write_str(EXE_SUFFIX)?;
2704        Ok(())
2705    }
2706}
2707
2708impl VersionRequest {
2709    /// Create a [`VersionRequest`] from [`VersionSpecifiers`].
2710    ///
2711    /// If the specifiers consist of a single `==` constraint, the version is parsed as a
2712    /// concrete version request (e.g., `MajorMinorPatch`) rather than a range.
2713    pub fn from_specifiers(specifiers: VersionSpecifiers, variant: PythonVariant) -> Self {
2714        if let [specifier] = specifiers.iter().as_slice()
2715            && specifier.operator() == &uv_pep440::Operator::Equal
2716            && let Ok(request) = Self::from_str(&specifier.version().to_string())
2717        {
2718            return request;
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            && variant != PythonVariant::Default
2824        {
2825            for i in 0..names.len() {
2826                let name = names[i].with_variant(variant);
2827                names.push(name);
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            && let Self::MajorMinor(major, minor, _) = self.clone().without_patch()
2932            && (major, minor) < (3, 13)
2933        {
2934            return Err(format!(
2935                "Python <3.13 does not support free-threading but {self} was requested."
2936            ));
2937        }
2938
2939        Ok(())
2940    }
2941
2942    /// Change this request into a request appropriate for the given [`PythonSource`].
2943    ///
2944    /// For example, if [`VersionRequest::Default`] is requested, it will be changed to
2945    /// [`VersionRequest::Any`] for sources that should allow non-default interpreters like
2946    /// free-threaded variants.
2947    #[must_use]
2948    fn into_request_for_source(self, source: PythonSource) -> Self {
2949        match self {
2950            Self::Default => match source {
2951                PythonSource::ParentInterpreter
2952                | PythonSource::CondaPrefix
2953                | PythonSource::BaseCondaPrefix
2954                | PythonSource::ProvidedPath
2955                | PythonSource::DiscoveredEnvironment
2956                | PythonSource::ActiveEnvironment => Self::Any,
2957                PythonSource::SearchPath
2958                | PythonSource::SearchPathFirst
2959                | PythonSource::Registry
2960                | PythonSource::MicrosoftStore
2961                | PythonSource::Managed => Self::Default,
2962            },
2963            _ => self,
2964        }
2965    }
2966
2967    /// Check if an installation matches the request, adjusting the request for the installation's
2968    /// source.
2969    pub(crate) fn matches_installation(&self, installation: &PythonInstallation) -> bool {
2970        let request = self.clone().into_request_for_source(installation.source);
2971        request.matches_interpreter(&installation.interpreter)
2972    }
2973
2974    /// Check if a interpreter matches the request.
2975    pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
2976        match self {
2977            Self::Any => true,
2978            // Do not use free-threaded interpreters by default
2979            Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
2980            Self::Major(major, variant) => {
2981                interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
2982            }
2983            Self::MajorMinor(major, minor, variant) => {
2984                (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
2985                    && variant.matches_interpreter(interpreter)
2986            }
2987            Self::MajorMinorPatch(major, minor, patch, variant) => {
2988                (
2989                    interpreter.python_major(),
2990                    interpreter.python_minor(),
2991                    interpreter.python_patch(),
2992                ) == (*major, *minor, *patch)
2993                    // When a patch version is included, we treat it as a request for a stable
2994                    // release
2995                    && interpreter.python_version().pre().is_none()
2996                    && variant.matches_interpreter(interpreter)
2997            }
2998            Self::Range(specifiers, variant) => {
2999                // If the specifier contains pre-releases, use the full version for comparison.
3000                // Otherwise, strip pre-release so that, e.g., `>=3.14` matches `3.14.0rc3`.
3001                let version = if specifiers
3002                    .iter()
3003                    .any(uv_pep440::VersionSpecifier::any_prerelease)
3004                {
3005                    Cow::Borrowed(interpreter.python_version())
3006                } else {
3007                    Cow::Owned(interpreter.python_version().only_release())
3008                };
3009                specifiers.contains(&version) && variant.matches_interpreter(interpreter)
3010            }
3011            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3012                let version = interpreter.python_version();
3013                let Some(interpreter_prerelease) = version.pre() else {
3014                    return false;
3015                };
3016                (
3017                    interpreter.python_major(),
3018                    interpreter.python_minor(),
3019                    interpreter_prerelease,
3020                ) == (*major, *minor, *prerelease)
3021                    && variant.matches_interpreter(interpreter)
3022            }
3023            Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3024                let version = interpreter.python_version();
3025                let Some(interpreter_prerelease) = version.pre() else {
3026                    return false;
3027                };
3028                (
3029                    interpreter.python_major(),
3030                    interpreter.python_minor(),
3031                    interpreter.python_patch(),
3032                    interpreter_prerelease,
3033                ) == (*major, *minor, *patch, *prerelease)
3034                    && variant.matches_interpreter(interpreter)
3035            }
3036        }
3037    }
3038
3039    /// Check if a version is compatible with the request.
3040    ///
3041    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
3042    /// avoid querying interpreters if it's clear it cannot fulfill the request.
3043    fn matches_version(&self, version: &PythonVersion) -> bool {
3044        match self {
3045            Self::Any | Self::Default => true,
3046            Self::Major(major, _) => version.major() == *major,
3047            Self::MajorMinor(major, minor, _) => {
3048                (version.major(), version.minor()) == (*major, *minor)
3049            }
3050            Self::MajorMinorPatch(major, minor, patch, _) => {
3051                (version.major(), version.minor(), version.patch())
3052                    == (*major, *minor, Some(*patch))
3053            }
3054            Self::Range(specifiers, _) => {
3055                // If the specifier contains pre-releases, use the full version for comparison.
3056                // Otherwise, strip pre-release so that, e.g., `>=3.14` matches `3.14.0rc3`.
3057                let version = if specifiers
3058                    .iter()
3059                    .any(uv_pep440::VersionSpecifier::any_prerelease)
3060                {
3061                    Cow::Borrowed(&version.version)
3062                } else {
3063                    Cow::Owned(version.version.only_release())
3064                };
3065                specifiers.contains(&version)
3066            }
3067            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3068                (version.major(), version.minor(), version.pre())
3069                    == (*major, *minor, Some(*prerelease))
3070            }
3071            Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3072                (
3073                    version.major(),
3074                    version.minor(),
3075                    version.patch(),
3076                    version.pre(),
3077                ) == (*major, *minor, Some(*patch), Some(*prerelease))
3078            }
3079        }
3080    }
3081
3082    /// Check if major and minor version segments are compatible with the request.
3083    ///
3084    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
3085    /// avoid querying interpreters if it's clear it cannot fulfill the request.
3086    fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
3087        match self {
3088            Self::Any | Self::Default => true,
3089            Self::Major(self_major, _) => *self_major == major,
3090            Self::MajorMinor(self_major, self_minor, _) => {
3091                (*self_major, *self_minor) == (major, minor)
3092            }
3093            Self::MajorMinorPatch(self_major, self_minor, _, _) => {
3094                (*self_major, *self_minor) == (major, minor)
3095            }
3096            Self::Range(specifiers, _) => {
3097                let range = release_specifiers_to_ranges(specifiers.clone());
3098                let Some((lower, upper)) = range.bounding_range() else {
3099                    return true;
3100                };
3101                let version = Version::new([u64::from(major), u64::from(minor)]);
3102
3103                let lower = LowerBound::new(lower.cloned());
3104                if !lower.major_minor().contains(&version) {
3105                    return false;
3106                }
3107
3108                let upper = UpperBound::new(upper.cloned());
3109                if !upper.major_minor().contains(&version) {
3110                    return false;
3111                }
3112
3113                true
3114            }
3115            Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
3116                (*self_major, *self_minor) == (major, minor)
3117            }
3118            Self::MajorMinorPatchPrerelease(self_major, self_minor, _, _, _) => {
3119                (*self_major, *self_minor) == (major, minor)
3120            }
3121        }
3122    }
3123
3124    /// Check if major, minor, patch, and prerelease version segments are compatible with the
3125    /// request.
3126    ///
3127    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
3128    /// avoid querying interpreters if it's clear it cannot fulfill the request.
3129    pub(crate) fn matches_major_minor_patch_prerelease(
3130        &self,
3131        major: u8,
3132        minor: u8,
3133        patch: u8,
3134        prerelease: Option<Prerelease>,
3135    ) -> bool {
3136        match self {
3137            Self::Any | Self::Default => true,
3138            Self::Major(self_major, _) => *self_major == major,
3139            Self::MajorMinor(self_major, self_minor, _) => {
3140                (*self_major, *self_minor) == (major, minor)
3141            }
3142            Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
3143                (*self_major, *self_minor, *self_patch) == (major, minor, patch)
3144                    // When a patch version is included, we treat it as a request for a stable
3145                    // release
3146                    && prerelease.is_none()
3147            }
3148            Self::Range(specifiers, _) => specifiers.contains(
3149                &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
3150                    .with_pre(prerelease),
3151            ),
3152            Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3153                // Pre-releases without a patch in the request match the zero patch version
3154                (*self_major, *self_minor, 0, Some(*self_prerelease))
3155                    == (major, minor, patch, prerelease)
3156            }
3157            Self::MajorMinorPatchPrerelease(
3158                self_major,
3159                self_minor,
3160                self_patch,
3161                self_prerelease,
3162                _,
3163            ) => {
3164                (
3165                    *self_major,
3166                    *self_minor,
3167                    *self_patch,
3168                    Some(*self_prerelease),
3169                ) == (major, minor, patch, prerelease)
3170            }
3171        }
3172    }
3173
3174    /// Check if a [`PythonInstallationKey`] is compatible with the request.
3175    ///
3176    /// WARNING: Use [`VersionRequest::matches_interpreter`] too. This method is only suitable to
3177    /// avoid querying interpreters if it's clear it cannot fulfill the request.
3178    pub(crate) fn matches_installation_key(&self, key: &PythonInstallationKey) -> bool {
3179        self.matches_major_minor_patch_prerelease(key.major, key.minor, key.patch, key.prerelease())
3180    }
3181
3182    /// Whether a patch version segment is present in the request.
3183    fn has_patch(&self) -> bool {
3184        match self {
3185            Self::Any | Self::Default => false,
3186            Self::Major(..) => false,
3187            Self::MajorMinor(..) => false,
3188            Self::MajorMinorPatch(..) => true,
3189            Self::MajorMinorPrerelease(..) => false,
3190            Self::MajorMinorPatchPrerelease(..) => true,
3191            Self::Range(_, _) => false,
3192        }
3193    }
3194
3195    /// Return a new [`VersionRequest`] without the patch version if possible.
3196    ///
3197    /// If the patch version is not present, the request is returned unchanged.
3198    #[must_use]
3199    fn without_patch(self) -> Self {
3200        match self {
3201            Self::Default => Self::Default,
3202            Self::Any => Self::Any,
3203            Self::Major(major, variant) => Self::Major(major, variant),
3204            Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3205            Self::MajorMinorPatch(major, minor, _, variant) => {
3206                Self::MajorMinor(major, minor, variant)
3207            }
3208            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3209                Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3210            }
3211            Self::MajorMinorPatchPrerelease(major, minor, _, prerelease, variant) => {
3212                Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3213            }
3214            Self::Range(_, _) => self,
3215        }
3216    }
3217
3218    /// Whether this request should allow selection of pre-release versions.
3219    pub(crate) fn allows_prereleases(&self) -> bool {
3220        match self {
3221            Self::Default => false,
3222            Self::Any => true,
3223            Self::Major(..) => false,
3224            Self::MajorMinor(..) => false,
3225            Self::MajorMinorPatch(..) => false,
3226            Self::MajorMinorPrerelease(..) => true,
3227            Self::MajorMinorPatchPrerelease(..) => true,
3228            Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3229        }
3230    }
3231
3232    /// Whether this request is for a debug Python variant.
3233    pub(crate) fn is_debug(&self) -> bool {
3234        match self {
3235            Self::Any | Self::Default => false,
3236            Self::Major(_, variant)
3237            | Self::MajorMinor(_, _, variant)
3238            | Self::MajorMinorPatch(_, _, _, variant)
3239            | Self::MajorMinorPrerelease(_, _, _, variant)
3240            | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3241            | Self::Range(_, variant) => variant.is_debug(),
3242        }
3243    }
3244
3245    /// Whether this request is for a free-threaded Python variant.
3246    fn is_freethreaded(&self) -> bool {
3247        match self {
3248            Self::Any | Self::Default => false,
3249            Self::Major(_, variant)
3250            | Self::MajorMinor(_, _, variant)
3251            | Self::MajorMinorPatch(_, _, _, variant)
3252            | Self::MajorMinorPrerelease(_, _, _, variant)
3253            | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3254            | Self::Range(_, variant) => variant.is_freethreaded(),
3255        }
3256    }
3257
3258    /// Return the [`PythonVariant`] of the request, if any.
3259    pub(crate) fn variant(&self) -> Option<PythonVariant> {
3260        match self {
3261            Self::Any => None,
3262            Self::Default => Some(PythonVariant::Default),
3263            Self::Major(_, variant)
3264            | Self::MajorMinor(_, _, variant)
3265            | Self::MajorMinorPatch(_, _, _, variant)
3266            | Self::MajorMinorPrerelease(_, _, _, variant)
3267            | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3268            | Self::Range(_, variant) => Some(*variant),
3269        }
3270    }
3271
3272    /// Convert this request into a concrete PEP 440 `Version` when possible.
3273    ///
3274    /// Returns `None` for non-concrete requests
3275    fn as_pep440_version(&self) -> Option<Version> {
3276        match self {
3277            Self::Default | Self::Any | Self::Range(_, _) => None,
3278            Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3279            Self::MajorMinor(major, minor, _) => {
3280                Some(Version::new([u64::from(*major), u64::from(*minor)]))
3281            }
3282            Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3283                u64::from(*major),
3284                u64::from(*minor),
3285                u64::from(*patch),
3286            ])),
3287            // Pre-releases without a patch use the zero patch version
3288            Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3289                Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3290            ),
3291            Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => Some(
3292                Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3293                    .with_pre(Some(*prerelease)),
3294            ),
3295        }
3296    }
3297
3298    /// Convert this request into [`VersionSpecifiers`] representing the range of compatible
3299    /// versions.
3300    ///
3301    /// Returns `None` for requests without version constraints (e.g., [`VersionRequest::Default`]
3302    /// and [`VersionRequest::Any`]).
3303    fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
3304        match self {
3305            Self::Default | Self::Any => None,
3306            Self::Major(major, _) => Some(VersionSpecifiers::from(
3307                VersionSpecifier::equals_star_version(Version::new([u64::from(*major)])),
3308            )),
3309            Self::MajorMinor(major, minor, _) => Some(VersionSpecifiers::from(
3310                VersionSpecifier::equals_star_version(Version::new([
3311                    u64::from(*major),
3312                    u64::from(*minor),
3313                ])),
3314            )),
3315            Self::MajorMinorPatch(major, minor, patch, _) => {
3316                Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3317                    Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)]),
3318                )))
3319            }
3320            Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3321                Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3322                    Version::new([u64::from(*major), u64::from(*minor), 0])
3323                        .with_pre(Some(*prerelease)),
3324                )))
3325            }
3326            Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3327                Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3328                    Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3329                        .with_pre(Some(*prerelease)),
3330                )))
3331            }
3332            Self::Range(specifiers, _) => Some(specifiers.clone()),
3333        }
3334    }
3335}
3336
3337impl FromStr for VersionRequest {
3338    type Err = Error;
3339
3340    fn from_str(s: &str) -> Result<Self, Self::Err> {
3341        /// Extract the variant from the end of a version request string, returning the prefix and
3342        /// the variant type.
3343        fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3344            // This cannot be a valid version, just error immediately
3345            if s.chars().all(char::is_alphabetic) {
3346                return Err(Error::InvalidVersionRequest(s.to_string()));
3347            }
3348
3349            let Some(mut start) = s.rfind(|c: char| c.is_ascii_digit()) else {
3350                return Ok((s, PythonVariant::Default));
3351            };
3352
3353            // Advance past the first digit
3354            start += 1;
3355
3356            // Ensure we're not out of bounds
3357            if start + 1 > s.len() {
3358                return Ok((s, PythonVariant::Default));
3359            }
3360
3361            let variant = &s[start..];
3362            let prefix = &s[..start];
3363
3364            // Strip a leading `+` if present
3365            let variant = variant.strip_prefix('+').unwrap_or(variant);
3366
3367            // TODO(zanieb): Special-case error for use of `dt` instead of `td`
3368
3369            // If there's not a valid variant, fallback to failure in [`Version::from_str`]
3370            let Ok(variant) = PythonVariant::from_str(variant) else {
3371                return Ok((s, PythonVariant::Default));
3372            };
3373
3374            Ok((prefix, variant))
3375        }
3376
3377        let (s, variant) = parse_variant(s)?;
3378        let Ok(version) = Version::from_str(s) else {
3379            return parse_version_specifiers_request(s, variant);
3380        };
3381
3382        // Split the release component if it uses the wheel tag format (e.g., `38`)
3383        let version = split_wheel_tag_release_version(version);
3384
3385        // We dont allow post or dev version here
3386        if version.post().is_some() || version.dev().is_some() {
3387            return Err(Error::InvalidVersionRequest(s.to_string()));
3388        }
3389
3390        // We don't allow local version suffixes unless they're variants, in which case they'd
3391        // already be stripped.
3392        if !version.local().is_empty() {
3393            return Err(Error::InvalidVersionRequest(s.to_string()));
3394        }
3395
3396        // Cast the release components into u8s since that's what we use in `VersionRequest`
3397        let Ok(release) = try_into_u8_slice(&version.release()) else {
3398            return Err(Error::InvalidVersionRequest(s.to_string()));
3399        };
3400
3401        let prerelease = version.pre();
3402
3403        match release.as_slice() {
3404            // e.g. `3
3405            [major] => {
3406                // Prereleases are not allowed here, e.g., `3rc1` doesn't make sense
3407                if prerelease.is_some() {
3408                    return Err(Error::InvalidVersionRequest(s.to_string()));
3409                }
3410                Ok(Self::Major(*major, variant))
3411            }
3412            // e.g. `3.12` or `312` or `3.13rc1`
3413            [major, minor] => {
3414                if let Some(prerelease) = prerelease {
3415                    return Ok(Self::MajorMinorPrerelease(
3416                        *major, *minor, prerelease, variant,
3417                    ));
3418                }
3419                Ok(Self::MajorMinor(*major, *minor, variant))
3420            }
3421            // e.g. `3.12.1`, `3.13.0rc1`, or `3.14.5rc1`
3422            [major, minor, patch] => {
3423                if let Some(prerelease) = prerelease {
3424                    if *patch == 0 {
3425                        return Ok(Self::MajorMinorPrerelease(
3426                            *major, *minor, prerelease, variant,
3427                        ));
3428                    }
3429                    return Ok(Self::MajorMinorPatchPrerelease(
3430                        *major, *minor, *patch, prerelease, variant,
3431                    ));
3432                }
3433                Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3434            }
3435            _ => Err(Error::InvalidVersionRequest(s.to_string())),
3436        }
3437    }
3438}
3439
3440impl FromStr for PythonVariant {
3441    type Err = ();
3442
3443    fn from_str(s: &str) -> Result<Self, Self::Err> {
3444        match s {
3445            "t" | "freethreaded" => Ok(Self::Freethreaded),
3446            "d" | "debug" => Ok(Self::Debug),
3447            "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3448            "gil" => Ok(Self::Gil),
3449            "gil+debug" => Ok(Self::GilDebug),
3450            "" => Ok(Self::Default),
3451            _ => Err(()),
3452        }
3453    }
3454}
3455
3456impl fmt::Display for PythonVariant {
3457    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3458        match self {
3459            Self::Default => f.write_str("default"),
3460            Self::Debug => f.write_str("debug"),
3461            Self::Freethreaded => f.write_str("freethreaded"),
3462            Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3463            Self::Gil => f.write_str("gil"),
3464            Self::GilDebug => f.write_str("gil+debug"),
3465        }
3466    }
3467}
3468
3469fn parse_version_specifiers_request(
3470    s: &str,
3471    variant: PythonVariant,
3472) -> Result<VersionRequest, Error> {
3473    let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3474        return Err(Error::InvalidVersionRequest(s.to_string()));
3475    };
3476    if specifiers.is_empty() {
3477        return Err(Error::InvalidVersionRequest(s.to_string()));
3478    }
3479    Ok(VersionRequest::from_specifiers(specifiers, variant))
3480}
3481
3482impl From<&PythonVersion> for VersionRequest {
3483    fn from(version: &PythonVersion) -> Self {
3484        Self::from_str(&version.string)
3485            .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3486    }
3487}
3488
3489impl fmt::Display for VersionRequest {
3490    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3491        match self {
3492            Self::Any => f.write_str("any"),
3493            Self::Default => f.write_str("default"),
3494            Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3495            Self::MajorMinor(major, minor, variant) => {
3496                write!(f, "{major}.{minor}{}", variant.display_suffix())
3497            }
3498            Self::MajorMinorPatch(major, minor, patch, variant) => {
3499                write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3500            }
3501            Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3502                write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3503            }
3504            Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3505                write!(
3506                    f,
3507                    "{major}.{minor}.{patch}{prerelease}{}",
3508                    variant.display_suffix()
3509                )
3510            }
3511            Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3512        }
3513    }
3514}
3515
3516impl fmt::Display for PythonRequest {
3517    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3518        match self {
3519            Self::Default => write!(f, "a default Python"),
3520            Self::Any => write!(f, "any Python"),
3521            Self::Version(version) => write!(f, "Python {version}"),
3522            Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3523            Self::File(path) => write!(f, "path `{}`", path.user_display()),
3524            Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3525            Self::Implementation(implementation) => {
3526                write!(f, "{}", implementation.pretty())
3527            }
3528            Self::ImplementationVersion(implementation, version) => {
3529                write!(f, "{} {version}", implementation.pretty())
3530            }
3531            Self::Key(request) => write!(f, "{request}"),
3532        }
3533    }
3534}
3535
3536impl fmt::Display for PythonSource {
3537    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3538        match self {
3539            Self::ProvidedPath => f.write_str("provided path"),
3540            Self::ActiveEnvironment => f.write_str("active virtual environment"),
3541            Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3542            Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3543            Self::SearchPath => f.write_str("search path"),
3544            Self::SearchPathFirst => f.write_str("first executable in the search path"),
3545            Self::Registry => f.write_str("registry"),
3546            Self::MicrosoftStore => f.write_str("Microsoft Store"),
3547            Self::Managed => f.write_str("managed installations"),
3548            Self::ParentInterpreter => f.write_str("parent interpreter"),
3549        }
3550    }
3551}
3552
3553impl PythonPreference {
3554    /// Return the sources that are considered when searching for a Python interpreter with this
3555    /// preference.
3556    fn sources(self) -> &'static [PythonSource] {
3557        match self {
3558            Self::OnlyManaged => &[PythonSource::Managed],
3559            Self::Managed => {
3560                if cfg!(windows) {
3561                    &[
3562                        PythonSource::Managed,
3563                        PythonSource::SearchPath,
3564                        PythonSource::Registry,
3565                    ]
3566                } else {
3567                    &[PythonSource::Managed, PythonSource::SearchPath]
3568                }
3569            }
3570            Self::System => {
3571                if cfg!(windows) {
3572                    &[
3573                        PythonSource::SearchPath,
3574                        PythonSource::Registry,
3575                        PythonSource::Managed,
3576                    ]
3577                } else {
3578                    &[PythonSource::SearchPath, PythonSource::Managed]
3579                }
3580            }
3581            Self::OnlySystem => {
3582                if cfg!(windows) {
3583                    &[PythonSource::SearchPath, PythonSource::Registry]
3584                } else {
3585                    &[PythonSource::SearchPath]
3586                }
3587            }
3588        }
3589    }
3590}
3591
3592impl fmt::Display for PythonPreference {
3593    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3594        f.write_str(match self {
3595            Self::OnlyManaged => "only managed",
3596            Self::Managed => "prefer managed",
3597            Self::System => "prefer system",
3598            Self::OnlySystem => "only system",
3599        })
3600    }
3601}
3602
3603impl DiscoveryPreferences {
3604    /// Return a string describing the sources that are considered when searching for Python with
3605    /// the given preferences.
3606    fn sources(&self, request: &PythonRequest) -> String {
3607        let python_sources = self
3608            .python_preference
3609            .sources()
3610            .iter()
3611            .map(ToString::to_string)
3612            .collect::<Vec<_>>();
3613        match self.environment_preference {
3614            EnvironmentPreference::Any => disjunction(
3615                &["virtual environments"]
3616                    .into_iter()
3617                    .chain(python_sources.iter().map(String::as_str))
3618                    .collect::<Vec<_>>(),
3619            ),
3620            EnvironmentPreference::ExplicitSystem => {
3621                if request.is_explicit_system() {
3622                    disjunction(
3623                        &["virtual environments"]
3624                            .into_iter()
3625                            .chain(python_sources.iter().map(String::as_str))
3626                            .collect::<Vec<_>>(),
3627                    )
3628                } else {
3629                    disjunction(&["virtual environments"])
3630                }
3631            }
3632            EnvironmentPreference::OnlySystem => disjunction(
3633                &python_sources
3634                    .iter()
3635                    .map(String::as_str)
3636                    .collect::<Vec<_>>(),
3637            ),
3638            EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3639        }
3640    }
3641}
3642
3643impl fmt::Display for PythonNotFound {
3644    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3645        let sources = DiscoveryPreferences {
3646            python_preference: self.python_preference,
3647            environment_preference: self.environment_preference,
3648        }
3649        .sources(&self.request);
3650
3651        match self.request {
3652            PythonRequest::Default | PythonRequest::Any => {
3653                write!(f, "No interpreter found in {sources}")
3654            }
3655            PythonRequest::File(_) => {
3656                write!(f, "No interpreter found at {}", self.request)
3657            }
3658            PythonRequest::Directory(_) => {
3659                write!(f, "No interpreter found in {}", self.request)
3660            }
3661            _ => {
3662                write!(f, "No interpreter found for {} in {sources}", self.request)
3663            }
3664        }
3665    }
3666}
3667
3668/// Join a series of items with `or` separators, making use of commas when necessary.
3669fn disjunction(items: &[&str]) -> String {
3670    match items.len() {
3671        0 => String::new(),
3672        1 => items[0].to_string(),
3673        2 => format!("{} or {}", items[0], items[1]),
3674        _ => {
3675            let last = items.last().unwrap();
3676            format!(
3677                "{}, or {}",
3678                items.iter().take(items.len() - 1).join(", "),
3679                last
3680            )
3681        }
3682    }
3683}
3684
3685fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3686    release
3687        .iter()
3688        .map(|x| match u8::try_from(*x) {
3689            Ok(x) => Ok(x),
3690            Err(e) => Err(e),
3691        })
3692        .collect()
3693}
3694
3695/// Convert a wheel tag formatted version (e.g., `38`) to multiple components (e.g., `3.8`).
3696///
3697/// The major version is always assumed to be a single digit 0-9. The minor version is all
3698/// the following content.
3699///
3700/// If not a wheel tag formatted version, the input is returned unchanged.
3701fn split_wheel_tag_release_version(version: Version) -> Version {
3702    let release = version.release();
3703    if release.len() != 1 {
3704        return version;
3705    }
3706
3707    let release = release[0].to_string();
3708    let mut chars = release.chars();
3709    let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3710        return version;
3711    };
3712
3713    let Ok(minor) = chars.as_str().parse::<u32>() else {
3714        return version;
3715    };
3716
3717    version.with_release([u64::from(major), u64::from(minor)])
3718}
3719
3720#[cfg(test)]
3721mod tests {
3722    use std::{cell::Cell, path::PathBuf, str::FromStr};
3723
3724    use assert_fs::{TempDir, prelude::*};
3725    use target_lexicon::{Aarch64Architecture, Architecture};
3726    use test_log::test;
3727    use uv_cache::Cache;
3728    use uv_distribution_types::RequiresPython;
3729    use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3730
3731    use crate::{
3732        discovery::{PythonRequest, VersionRequest},
3733        downloads::{ArchRequest, PythonDownloadRequest},
3734        implementation::ImplementationName,
3735    };
3736    use uv_platform::{Arch, Libc, Os};
3737
3738    use super::{
3739        DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonSource,
3740        PythonVariant, QueryStrategy, python_installations_from_executables,
3741    };
3742
3743    #[test]
3744    fn sequential_query_strategy_does_not_prefetch_executables() -> anyhow::Result<()> {
3745        let cache = Cache::temp()?;
3746        let pulls = Cell::new(0);
3747        let executables = (0..2).map(|_| {
3748            pulls.set(pulls.get() + 1);
3749            Err::<(PythonSource, PathBuf), _>(Error::SourceNotAllowed(
3750                PythonRequest::Default,
3751                PythonSource::SearchPath,
3752                PythonPreference::OnlyManaged,
3753            ))
3754        });
3755
3756        let mut installations =
3757            python_installations_from_executables(executables, &cache, QueryStrategy::Sequential);
3758
3759        assert_eq!(pulls.get(), 0);
3760        assert!(installations.next().is_some_and(|result| result.is_err()));
3761        assert_eq!(pulls.get(), 1);
3762
3763        Ok(())
3764    }
3765
3766    #[test]
3767    fn interpreter_request_from_str() {
3768        assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3769        assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
3770        assert_eq!(
3771            PythonRequest::parse("3.12"),
3772            PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
3773        );
3774        assert_eq!(
3775            PythonRequest::parse(">=3.12"),
3776            PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3777        );
3778        assert_eq!(
3779            PythonRequest::parse(">=3.12,<3.13"),
3780            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3781        );
3782        assert_eq!(
3783            PythonRequest::parse(">=3.12,<3.13"),
3784            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3785        );
3786
3787        assert_eq!(
3788            PythonRequest::parse("3.13.0a1"),
3789            PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3790        );
3791        assert_eq!(
3792            PythonRequest::parse("3.13.0b5"),
3793            PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3794        );
3795        assert_eq!(
3796            PythonRequest::parse("3.13.0rc1"),
3797            PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3798        );
3799        assert_eq!(
3800            PythonRequest::parse("3.13.1rc1"),
3801            PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3802            "Pre-release version requests require a patch version of zero"
3803        );
3804        assert_eq!(
3805            PythonRequest::parse("3rc1"),
3806            PythonRequest::ExecutableName("3rc1".to_string()),
3807            "Pre-release version requests require a minor version"
3808        );
3809
3810        assert_eq!(
3811            PythonRequest::parse("cpython"),
3812            PythonRequest::Implementation(ImplementationName::CPython)
3813        );
3814
3815        assert_eq!(
3816            PythonRequest::parse("cpython3.12.2"),
3817            PythonRequest::ImplementationVersion(
3818                ImplementationName::CPython,
3819                VersionRequest::from_str("3.12.2").unwrap(),
3820            )
3821        );
3822
3823        assert_eq!(
3824            PythonRequest::parse("cpython-3.13.2"),
3825            PythonRequest::Key(PythonDownloadRequest {
3826                version: Some(VersionRequest::MajorMinorPatch(
3827                    3,
3828                    13,
3829                    2,
3830                    PythonVariant::Default
3831                )),
3832                implementation: Some(ImplementationName::CPython),
3833                arch: None,
3834                os: None,
3835                libc: None,
3836                build: None,
3837                prereleases: None
3838            })
3839        );
3840        assert_eq!(
3841            PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3842            PythonRequest::Key(PythonDownloadRequest {
3843                version: Some(VersionRequest::MajorMinorPatch(
3844                    3,
3845                    13,
3846                    2,
3847                    PythonVariant::Default
3848                )),
3849                implementation: Some(ImplementationName::CPython),
3850                arch: Some(ArchRequest::Explicit(Arch::new(
3851                    Architecture::Aarch64(Aarch64Architecture::Aarch64),
3852                    None
3853                ))),
3854                os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3855                libc: Some(Libc::None),
3856                build: None,
3857                prereleases: None
3858            })
3859        );
3860        assert_eq!(
3861            PythonRequest::parse("any-3.13.2"),
3862            PythonRequest::Key(PythonDownloadRequest {
3863                version: Some(VersionRequest::MajorMinorPatch(
3864                    3,
3865                    13,
3866                    2,
3867                    PythonVariant::Default
3868                )),
3869                implementation: None,
3870                arch: None,
3871                os: None,
3872                libc: None,
3873                build: None,
3874                prereleases: None
3875            })
3876        );
3877        assert_eq!(
3878            PythonRequest::parse("any-3.13.2-any-aarch64"),
3879            PythonRequest::Key(PythonDownloadRequest {
3880                version: Some(VersionRequest::MajorMinorPatch(
3881                    3,
3882                    13,
3883                    2,
3884                    PythonVariant::Default
3885                )),
3886                implementation: None,
3887                arch: Some(ArchRequest::Explicit(Arch::new(
3888                    Architecture::Aarch64(Aarch64Architecture::Aarch64),
3889                    None
3890                ))),
3891                os: None,
3892                libc: None,
3893                build: None,
3894                prereleases: None
3895            })
3896        );
3897
3898        assert_eq!(
3899            PythonRequest::parse("pypy"),
3900            PythonRequest::Implementation(ImplementationName::PyPy)
3901        );
3902        assert_eq!(
3903            PythonRequest::parse("pp"),
3904            PythonRequest::Implementation(ImplementationName::PyPy)
3905        );
3906        assert_eq!(
3907            PythonRequest::parse("graalpy"),
3908            PythonRequest::Implementation(ImplementationName::GraalPy)
3909        );
3910        assert_eq!(
3911            PythonRequest::parse("gp"),
3912            PythonRequest::Implementation(ImplementationName::GraalPy)
3913        );
3914        assert_eq!(
3915            PythonRequest::parse("cp"),
3916            PythonRequest::Implementation(ImplementationName::CPython)
3917        );
3918        assert_eq!(
3919            PythonRequest::parse("pypy3.10"),
3920            PythonRequest::ImplementationVersion(
3921                ImplementationName::PyPy,
3922                VersionRequest::from_str("3.10").unwrap(),
3923            )
3924        );
3925        assert_eq!(
3926            PythonRequest::parse("pp310"),
3927            PythonRequest::ImplementationVersion(
3928                ImplementationName::PyPy,
3929                VersionRequest::from_str("3.10").unwrap(),
3930            )
3931        );
3932        assert_eq!(
3933            PythonRequest::parse("graalpy3.10"),
3934            PythonRequest::ImplementationVersion(
3935                ImplementationName::GraalPy,
3936                VersionRequest::from_str("3.10").unwrap(),
3937            )
3938        );
3939        assert_eq!(
3940            PythonRequest::parse("gp310"),
3941            PythonRequest::ImplementationVersion(
3942                ImplementationName::GraalPy,
3943                VersionRequest::from_str("3.10").unwrap(),
3944            )
3945        );
3946        assert_eq!(
3947            PythonRequest::parse("cp38"),
3948            PythonRequest::ImplementationVersion(
3949                ImplementationName::CPython,
3950                VersionRequest::from_str("3.8").unwrap(),
3951            )
3952        );
3953        assert_eq!(
3954            PythonRequest::parse("pypy@3.10"),
3955            PythonRequest::ImplementationVersion(
3956                ImplementationName::PyPy,
3957                VersionRequest::from_str("3.10").unwrap(),
3958            )
3959        );
3960        assert_eq!(
3961            PythonRequest::parse("pypy310"),
3962            PythonRequest::ImplementationVersion(
3963                ImplementationName::PyPy,
3964                VersionRequest::from_str("3.10").unwrap(),
3965            )
3966        );
3967        assert_eq!(
3968            PythonRequest::parse("graalpy@3.10"),
3969            PythonRequest::ImplementationVersion(
3970                ImplementationName::GraalPy,
3971                VersionRequest::from_str("3.10").unwrap(),
3972            )
3973        );
3974        assert_eq!(
3975            PythonRequest::parse("graalpy310"),
3976            PythonRequest::ImplementationVersion(
3977                ImplementationName::GraalPy,
3978                VersionRequest::from_str("3.10").unwrap(),
3979            )
3980        );
3981
3982        let tempdir = TempDir::new().unwrap();
3983        assert_eq!(
3984            PythonRequest::parse(tempdir.path().to_str().unwrap()),
3985            PythonRequest::Directory(tempdir.path().to_path_buf()),
3986            "An existing directory is treated as a directory"
3987        );
3988        assert_eq!(
3989            PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3990            PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3991            "A path that does not exist is treated as a file"
3992        );
3993        tempdir.child("bar").touch().unwrap();
3994        assert_eq!(
3995            PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3996            PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3997            "An existing file is treated as a file"
3998        );
3999        assert_eq!(
4000            PythonRequest::parse("./foo"),
4001            PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
4002            "A string with a file system separator is treated as a file"
4003        );
4004        assert_eq!(
4005            PythonRequest::parse("3.13t"),
4006            PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
4007        );
4008    }
4009
4010    #[test]
4011    fn discovery_sources_prefer_system_orders_search_path_first() {
4012        let preferences = DiscoveryPreferences {
4013            python_preference: PythonPreference::System,
4014            environment_preference: EnvironmentPreference::OnlySystem,
4015        };
4016        let sources = preferences.sources(&PythonRequest::Default);
4017
4018        if cfg!(windows) {
4019            assert_eq!(sources, "search path, registry, or managed installations");
4020        } else {
4021            assert_eq!(sources, "search path or managed installations");
4022        }
4023    }
4024
4025    #[test]
4026    fn discovery_sources_only_system_matches_platform_order() {
4027        let preferences = DiscoveryPreferences {
4028            python_preference: PythonPreference::OnlySystem,
4029            environment_preference: EnvironmentPreference::OnlySystem,
4030        };
4031        let sources = preferences.sources(&PythonRequest::Default);
4032
4033        if cfg!(windows) {
4034            assert_eq!(sources, "search path or registry");
4035        } else {
4036            assert_eq!(sources, "search path");
4037        }
4038    }
4039
4040    #[test]
4041    fn interpreter_request_to_canonical_string() {
4042        assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
4043        assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
4044        assert_eq!(
4045            PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
4046            "3.12"
4047        );
4048        assert_eq!(
4049            PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
4050                .to_canonical_string(),
4051            ">=3.12"
4052        );
4053        assert_eq!(
4054            PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
4055                .to_canonical_string(),
4056            ">=3.12, <3.13"
4057        );
4058
4059        assert_eq!(
4060            PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
4061                .to_canonical_string(),
4062            "3.13a1"
4063        );
4064
4065        assert_eq!(
4066            PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
4067                .to_canonical_string(),
4068            "3.13b5"
4069        );
4070
4071        assert_eq!(
4072            PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
4073                .to_canonical_string(),
4074            "3.13rc1"
4075        );
4076
4077        assert_eq!(
4078            PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
4079                .to_canonical_string(),
4080            "3.13rc4"
4081        );
4082
4083        assert_eq!(
4084            PythonRequest::Version(VersionRequest::from_str("3.14.5rc1").unwrap())
4085                .to_canonical_string(),
4086            "3.14.5rc1"
4087        );
4088
4089        assert_eq!(
4090            PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
4091            "foo"
4092        );
4093        assert_eq!(
4094            PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
4095            "cpython"
4096        );
4097        assert_eq!(
4098            PythonRequest::ImplementationVersion(
4099                ImplementationName::CPython,
4100                VersionRequest::from_str("3.12.2").unwrap(),
4101            )
4102            .to_canonical_string(),
4103            "cpython@3.12.2"
4104        );
4105        assert_eq!(
4106            PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
4107            "pypy"
4108        );
4109        assert_eq!(
4110            PythonRequest::ImplementationVersion(
4111                ImplementationName::PyPy,
4112                VersionRequest::from_str("3.10").unwrap(),
4113            )
4114            .to_canonical_string(),
4115            "pypy@3.10"
4116        );
4117        assert_eq!(
4118            PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
4119            "graalpy"
4120        );
4121        assert_eq!(
4122            PythonRequest::ImplementationVersion(
4123                ImplementationName::GraalPy,
4124                VersionRequest::from_str("3.10").unwrap(),
4125            )
4126            .to_canonical_string(),
4127            "graalpy@3.10"
4128        );
4129
4130        let tempdir = TempDir::new().unwrap();
4131        assert_eq!(
4132            PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
4133            tempdir.path().to_str().unwrap(),
4134            "An existing directory is treated as a directory"
4135        );
4136        assert_eq!(
4137            PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
4138            tempdir.child("foo").path().to_str().unwrap(),
4139            "A path that does not exist is treated as a file"
4140        );
4141        tempdir.child("bar").touch().unwrap();
4142        assert_eq!(
4143            PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
4144            tempdir.child("bar").path().to_str().unwrap(),
4145            "An existing file is treated as a file"
4146        );
4147        assert_eq!(
4148            PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
4149            "./foo",
4150            "A string with a file system separator is treated as a file"
4151        );
4152    }
4153
4154    #[test]
4155    fn version_request_from_str() {
4156        assert_eq!(
4157            VersionRequest::from_str("3").unwrap(),
4158            VersionRequest::Major(3, PythonVariant::Default)
4159        );
4160        assert_eq!(
4161            VersionRequest::from_str("3.12").unwrap(),
4162            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4163        );
4164        assert_eq!(
4165            VersionRequest::from_str("3.12.1").unwrap(),
4166            VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4167        );
4168        assert!(VersionRequest::from_str("1.foo.1").is_err());
4169        assert_eq!(
4170            VersionRequest::from_str("3").unwrap(),
4171            VersionRequest::Major(3, PythonVariant::Default)
4172        );
4173        assert_eq!(
4174            VersionRequest::from_str("38").unwrap(),
4175            VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
4176        );
4177        assert_eq!(
4178            VersionRequest::from_str("312").unwrap(),
4179            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4180        );
4181        assert_eq!(
4182            VersionRequest::from_str("3100").unwrap(),
4183            VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
4184        );
4185        assert_eq!(
4186            VersionRequest::from_str("3.13a1").unwrap(),
4187            VersionRequest::MajorMinorPrerelease(
4188                3,
4189                13,
4190                Prerelease {
4191                    kind: PrereleaseKind::Alpha,
4192                    number: 1
4193                },
4194                PythonVariant::Default
4195            )
4196        );
4197        assert_eq!(
4198            VersionRequest::from_str("313b1").unwrap(),
4199            VersionRequest::MajorMinorPrerelease(
4200                3,
4201                13,
4202                Prerelease {
4203                    kind: PrereleaseKind::Beta,
4204                    number: 1
4205                },
4206                PythonVariant::Default
4207            )
4208        );
4209        assert_eq!(
4210            VersionRequest::from_str("3.13.0b2").unwrap(),
4211            VersionRequest::MajorMinorPrerelease(
4212                3,
4213                13,
4214                Prerelease {
4215                    kind: PrereleaseKind::Beta,
4216                    number: 2
4217                },
4218                PythonVariant::Default
4219            )
4220        );
4221        assert_eq!(
4222            VersionRequest::from_str("3.13.0rc3").unwrap(),
4223            VersionRequest::MajorMinorPrerelease(
4224                3,
4225                13,
4226                Prerelease {
4227                    kind: PrereleaseKind::Rc,
4228                    number: 3
4229                },
4230                PythonVariant::Default
4231            )
4232        );
4233        assert!(
4234            matches!(
4235                VersionRequest::from_str("3rc1"),
4236                Err(Error::InvalidVersionRequest(_))
4237            ),
4238            "Pre-release version requests require a minor version"
4239        );
4240        assert_eq!(
4241            VersionRequest::from_str("3.14.5rc1").unwrap(),
4242            VersionRequest::MajorMinorPatchPrerelease(
4243                3,
4244                14,
4245                5,
4246                Prerelease {
4247                    kind: PrereleaseKind::Rc,
4248                    number: 1
4249                },
4250                PythonVariant::Default
4251            ),
4252            "Pre-release version requests with a non-zero patch are allowed (e.g., `3.14.5rc1`)"
4253        );
4254        assert_eq!(
4255            VersionRequest::from_str("3.13.2rc1").unwrap(),
4256            VersionRequest::MajorMinorPatchPrerelease(
4257                3,
4258                13,
4259                2,
4260                Prerelease {
4261                    kind: PrereleaseKind::Rc,
4262                    number: 1
4263                },
4264                PythonVariant::Default
4265            )
4266        );
4267        assert!(
4268            matches!(
4269                VersionRequest::from_str("3.12-dev"),
4270                Err(Error::InvalidVersionRequest(_))
4271            ),
4272            "Development version segments are not allowed"
4273        );
4274        assert!(
4275            matches!(
4276                VersionRequest::from_str("3.12+local"),
4277                Err(Error::InvalidVersionRequest(_))
4278            ),
4279            "Local version segments are not allowed"
4280        );
4281        assert!(
4282            matches!(
4283                VersionRequest::from_str("3.12.post0"),
4284                Err(Error::InvalidVersionRequest(_))
4285            ),
4286            "Post version segments are not allowed"
4287        );
4288        assert!(
4289            // Test for overflow
4290            matches!(
4291                VersionRequest::from_str("31000"),
4292                Err(Error::InvalidVersionRequest(_))
4293            )
4294        );
4295        assert_eq!(
4296            VersionRequest::from_str("3t").unwrap(),
4297            VersionRequest::Major(3, PythonVariant::Freethreaded)
4298        );
4299        assert_eq!(
4300            VersionRequest::from_str("313t").unwrap(),
4301            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4302        );
4303        assert_eq!(
4304            VersionRequest::from_str("3.13t").unwrap(),
4305            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4306        );
4307        assert_eq!(
4308            VersionRequest::from_str(">=3.13t").unwrap(),
4309            VersionRequest::Range(
4310                VersionSpecifiers::from_str(">=3.13").unwrap(),
4311                PythonVariant::Freethreaded
4312            )
4313        );
4314        assert_eq!(
4315            VersionRequest::from_str(">=3.13").unwrap(),
4316            VersionRequest::Range(
4317                VersionSpecifiers::from_str(">=3.13").unwrap(),
4318                PythonVariant::Default
4319            )
4320        );
4321        assert_eq!(
4322            VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4323            VersionRequest::Range(
4324                VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4325                PythonVariant::Freethreaded
4326            )
4327        );
4328        assert!(matches!(
4329            VersionRequest::from_str("3.13tt"),
4330            Err(Error::InvalidVersionRequest(_))
4331        ));
4332        assert!(matches!(
4333            VersionRequest::from_str("3.12²t"),
4334            Err(Error::InvalidVersionRequest(_))
4335        ));
4336
4337        // `==` specifiers are parsed as concrete version requests via `from_specifiers`
4338        assert_eq!(
4339            VersionRequest::from_str("==3.12").unwrap(),
4340            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4341        );
4342        assert_eq!(
4343            VersionRequest::from_str("==3.12.1").unwrap(),
4344            VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4345        );
4346    }
4347
4348    #[test]
4349    fn version_request_from_specifiers() {
4350        // A single `==` specifier is parsed as a concrete version request
4351        assert_eq!(
4352            VersionRequest::from_specifiers(
4353                VersionSpecifiers::from_str("==3.12").unwrap(),
4354                PythonVariant::Default
4355            ),
4356            VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4357        );
4358        assert_eq!(
4359            VersionRequest::from_specifiers(
4360                VersionSpecifiers::from_str("==3.12.1").unwrap(),
4361                PythonVariant::Default
4362            ),
4363            VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4364        );
4365
4366        // Wildcard `==` specifiers remain as ranges
4367        assert_eq!(
4368            VersionRequest::from_specifiers(
4369                VersionSpecifiers::from_str("==3.12.*").unwrap(),
4370                PythonVariant::Default
4371            ),
4372            VersionRequest::Range(
4373                VersionSpecifiers::from_str("==3.12.*").unwrap(),
4374                PythonVariant::Default
4375            )
4376        );
4377
4378        // Range specifiers remain as ranges
4379        assert_eq!(
4380            VersionRequest::from_specifiers(
4381                VersionSpecifiers::from_str(">=3.12").unwrap(),
4382                PythonVariant::Default
4383            ),
4384            VersionRequest::Range(
4385                VersionSpecifiers::from_str(">=3.12").unwrap(),
4386                PythonVariant::Default
4387            )
4388        );
4389
4390        // Multi-specifier constraints remain as ranges
4391        assert_eq!(
4392            VersionRequest::from_specifiers(
4393                VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4394                PythonVariant::Default
4395            ),
4396            VersionRequest::Range(
4397                VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4398                PythonVariant::Default
4399            )
4400        );
4401    }
4402
4403    #[test]
4404    fn executable_names_from_request() {
4405        fn case(request: &str, expected: &[&str]) {
4406            let (implementation, version) = match PythonRequest::parse(request) {
4407                PythonRequest::Any => (None, VersionRequest::Any),
4408                PythonRequest::Default => (None, VersionRequest::Default),
4409                PythonRequest::Version(version) => (None, version),
4410                PythonRequest::ImplementationVersion(implementation, version) => {
4411                    (Some(implementation), version)
4412                }
4413                PythonRequest::Implementation(implementation) => {
4414                    (Some(implementation), VersionRequest::Default)
4415                }
4416                result => {
4417                    panic!("Test cases should request versions or implementations; got {result:?}")
4418                }
4419            };
4420
4421            let result: Vec<_> = version
4422                .executable_names(implementation.as_ref())
4423                .into_iter()
4424                .map(|name| name.to_string())
4425                .collect();
4426
4427            let expected: Vec<_> = expected
4428                .iter()
4429                .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4430                .collect();
4431
4432            assert_eq!(result, expected, "mismatch for case \"{request}\"");
4433        }
4434
4435        case(
4436            "any",
4437            &[
4438                "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4439                "pyodide", "pyodide3",
4440            ],
4441        );
4442
4443        case("default", &["python", "python3"]);
4444
4445        case("3", &["python3", "python"]);
4446
4447        case("4", &["python4", "python"]);
4448
4449        case("3.13", &["python3.13", "python3", "python"]);
4450
4451        case("pypy", &["pypy", "pypy3", "python", "python3"]);
4452
4453        case(
4454            "pypy@3.10",
4455            &[
4456                "pypy3.10",
4457                "pypy3",
4458                "pypy",
4459                "python3.10",
4460                "python3",
4461                "python",
4462            ],
4463        );
4464
4465        case(
4466            "3.13t",
4467            &[
4468                "python3.13t",
4469                "python3.13",
4470                "python3t",
4471                "python3",
4472                "pythont",
4473                "python",
4474            ],
4475        );
4476        case("3t", &["python3t", "python3", "pythont", "python"]);
4477
4478        case(
4479            "3.13.2",
4480            &["python3.13.2", "python3.13", "python3", "python"],
4481        );
4482
4483        case(
4484            "3.13rc2",
4485            &["python3.13rc2", "python3.13", "python3", "python"],
4486        );
4487    }
4488
4489    #[test]
4490    fn test_try_split_prefix_and_version() {
4491        assert!(matches!(
4492            PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4493            Ok(None),
4494        ));
4495        assert!(matches!(
4496            PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4497            Ok(Some(_)),
4498        ));
4499        assert!(matches!(
4500            PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4501            Ok(Some(_)),
4502        ));
4503        assert!(matches!(
4504            PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4505            Ok(None),
4506        ));
4507        // Version parsing errors are only raised if @ is present.
4508        assert!(
4509            PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4510        );
4511        // @ is not allowed if the prefix is empty.
4512        assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4513    }
4514
4515    #[test]
4516    fn version_request_as_pep440_version() {
4517        // Non-concrete requests return `None`
4518        assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4519        assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4520        assert_eq!(
4521            VersionRequest::from_str(">=3.10")
4522                .unwrap()
4523                .as_pep440_version(),
4524            None
4525        );
4526
4527        // `VersionRequest::Major`
4528        assert_eq!(
4529            VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4530            Some(Version::from_str("3").unwrap())
4531        );
4532
4533        // `VersionRequest::MajorMinor`
4534        assert_eq!(
4535            VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4536            Some(Version::from_str("3.12").unwrap())
4537        );
4538
4539        // `VersionRequest::MajorMinorPatch`
4540        assert_eq!(
4541            VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4542            Some(Version::from_str("3.12.5").unwrap())
4543        );
4544
4545        // `VersionRequest::MajorMinorPrerelease`
4546        assert_eq!(
4547            VersionRequest::MajorMinorPrerelease(
4548                3,
4549                14,
4550                Prerelease {
4551                    kind: PrereleaseKind::Alpha,
4552                    number: 1
4553                },
4554                PythonVariant::Default
4555            )
4556            .as_pep440_version(),
4557            Some(Version::from_str("3.14.0a1").unwrap())
4558        );
4559        assert_eq!(
4560            VersionRequest::MajorMinorPrerelease(
4561                3,
4562                14,
4563                Prerelease {
4564                    kind: PrereleaseKind::Beta,
4565                    number: 2
4566                },
4567                PythonVariant::Default
4568            )
4569            .as_pep440_version(),
4570            Some(Version::from_str("3.14.0b2").unwrap())
4571        );
4572        assert_eq!(
4573            VersionRequest::MajorMinorPrerelease(
4574                3,
4575                13,
4576                Prerelease {
4577                    kind: PrereleaseKind::Rc,
4578                    number: 3
4579                },
4580                PythonVariant::Default
4581            )
4582            .as_pep440_version(),
4583            Some(Version::from_str("3.13.0rc3").unwrap())
4584        );
4585
4586        // Variant is ignored
4587        assert_eq!(
4588            VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4589            Some(Version::from_str("3").unwrap())
4590        );
4591        assert_eq!(
4592            VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4593            Some(Version::from_str("3.13").unwrap())
4594        );
4595    }
4596
4597    #[test]
4598    fn python_request_as_pep440_version() {
4599        // `PythonRequest::Any` and `PythonRequest::Default` return `None`
4600        assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4601        assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4602
4603        // `PythonRequest::Version` delegates to `VersionRequest`
4604        assert_eq!(
4605            PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4606                .as_pep440_version(),
4607            Some(Version::from_str("3.11").unwrap())
4608        );
4609
4610        // `PythonRequest::ImplementationVersion` extracts version
4611        assert_eq!(
4612            PythonRequest::ImplementationVersion(
4613                ImplementationName::CPython,
4614                VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4615            )
4616            .as_pep440_version(),
4617            Some(Version::from_str("3.12.1").unwrap())
4618        );
4619
4620        // `PythonRequest::Implementation` returns `None` (no version)
4621        assert_eq!(
4622            PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4623            None
4624        );
4625
4626        // `PythonRequest::Key` with version
4627        assert_eq!(
4628            PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4629            Some(Version::from_str("3.13.2").unwrap())
4630        );
4631
4632        // `PythonRequest::Key` without version returns `None`
4633        assert_eq!(
4634            PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4635            None
4636        );
4637
4638        // Range versions return `None`
4639        assert_eq!(
4640            PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4641            None
4642        );
4643    }
4644
4645    #[test]
4646    fn intersects_requires_python_exact() {
4647        let requires_python =
4648            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4649
4650        assert!(PythonRequest::parse("3.12").intersects_requires_python(&requires_python));
4651        assert!(!PythonRequest::parse("3.11").intersects_requires_python(&requires_python));
4652    }
4653
4654    #[test]
4655    fn intersects_requires_python_major() {
4656        let requires_python =
4657            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4658
4659        // `3` overlaps with `>=3.12` (e.g., 3.12, 3.13, ... are all Python 3)
4660        assert!(PythonRequest::parse("3").intersects_requires_python(&requires_python));
4661        // `2` does not overlap with `>=3.12`
4662        assert!(!PythonRequest::parse("2").intersects_requires_python(&requires_python));
4663    }
4664
4665    #[test]
4666    fn intersects_requires_python_range() {
4667        let requires_python =
4668            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4669
4670        assert!(PythonRequest::parse(">=3.12,<3.13").intersects_requires_python(&requires_python));
4671        assert!(!PythonRequest::parse(">=3.10,<3.12").intersects_requires_python(&requires_python));
4672    }
4673
4674    #[test]
4675    fn intersects_requires_python_implementation_range() {
4676        let requires_python =
4677            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4678
4679        assert!(
4680            PythonRequest::parse("cpython@>=3.12,<3.13")
4681                .intersects_requires_python(&requires_python)
4682        );
4683        assert!(
4684            !PythonRequest::parse("cpython@>=3.10,<3.12")
4685                .intersects_requires_python(&requires_python)
4686        );
4687    }
4688
4689    #[test]
4690    fn intersects_requires_python_no_version() {
4691        let requires_python =
4692            RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4693
4694        // Requests without version constraints are always compatible
4695        assert!(PythonRequest::Any.intersects_requires_python(&requires_python));
4696        assert!(PythonRequest::Default.intersects_requires_python(&requires_python));
4697        assert!(
4698            PythonRequest::Implementation(ImplementationName::CPython)
4699                .intersects_requires_python(&requires_python)
4700        );
4701    }
4702}