Skip to main content

uv_python/
discovery.rs

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