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