Skip to main content

uv_python/
discovery.rs

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