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