Skip to main content

uv_python/
discovery.rs

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