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