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