1use itertools::{Either, Itertools};
2use owo_colors::AnsiColors;
3use regex::Regex;
4use reqwest_retry::policies::ExponentialBackoff;
5use rustc_hash::{FxBuildHasher, FxHashSet};
6use same_file::is_same_file;
7use std::borrow::Cow;
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::BaseClient;
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_preview::Preview;
24use uv_static::EnvVars;
25use uv_warnings::anstream;
26use uv_warnings::warn_user_once;
27use which::{which, which_all};
28
29use crate::downloads::{ManagedPythonDownloadList, PlatformRequest, PythonDownloadRequest};
30use crate::implementation::ImplementationName;
31use crate::installation::{PythonInstallation, PythonInstallationKey};
32use crate::interpreter::Error as InterpreterError;
33use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
34use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
35#[cfg(windows)]
36use crate::microsoft_store::find_microsoft_store_pythons;
37use crate::python_version::python_build_versions_from_env;
38use crate::virtualenv::Error as VirtualEnvError;
39use crate::virtualenv::{
40 CondaEnvironmentKind, conda_environment_from_env, virtualenv_from_env,
41 virtualenv_from_working_dir, virtualenv_python_executable,
42};
43#[cfg(windows)]
44use crate::windows_registry::{WindowsPython, registry_pythons};
45use crate::{BrokenLink, Interpreter, PythonVersion};
46
47#[derive(Debug, Clone, Eq, Default)]
51pub enum PythonRequest {
52 #[default]
57 Default,
58 Any,
60 Version(VersionRequest),
62 Directory(PathBuf),
64 File(PathBuf),
66 ExecutableName(String),
68 Implementation(ImplementationName),
70 ImplementationVersion(ImplementationName, VersionRequest),
72 Key(PythonDownloadRequest),
75}
76
77impl PartialEq for PythonRequest {
78 fn eq(&self, other: &Self) -> bool {
79 self.to_canonical_string() == other.to_canonical_string()
80 }
81}
82
83impl std::hash::Hash for PythonRequest {
84 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
85 self.to_canonical_string().hash(state);
86 }
87}
88
89impl<'a> serde::Deserialize<'a> for PythonRequest {
90 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91 where
92 D: serde::Deserializer<'a>,
93 {
94 let s = <Cow<'_, str>>::deserialize(deserializer)?;
95 Ok(Self::parse(&s))
96 }
97}
98
99impl serde::Serialize for PythonRequest {
100 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
101 where
102 S: serde::Serializer,
103 {
104 let s = self.to_canonical_string();
105 serializer.serialize_str(&s)
106 }
107}
108
109#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
110#[serde(deny_unknown_fields, rename_all = "kebab-case")]
111#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113pub enum PythonPreference {
114 OnlyManaged,
116 #[default]
117 Managed,
122 System,
126 OnlySystem,
128}
129
130#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
131#[serde(deny_unknown_fields, rename_all = "kebab-case")]
132#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
133#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
134pub enum PythonDownloads {
135 #[default]
137 #[serde(alias = "auto")]
138 Automatic,
139 Manual,
141 Never,
143}
144
145impl FromStr for PythonDownloads {
146 type Err = String;
147
148 fn from_str(s: &str) -> Result<Self, Self::Err> {
149 match s.to_ascii_lowercase().as_str() {
150 "auto" | "automatic" | "true" | "1" => Ok(Self::Automatic),
151 "manual" => Ok(Self::Manual),
152 "never" | "false" | "0" => Ok(Self::Never),
153 _ => Err(format!("Invalid value for `python-download`: '{s}'")),
154 }
155 }
156}
157
158impl From<bool> for PythonDownloads {
159 fn from(value: bool) -> Self {
160 if value { Self::Automatic } else { Self::Never }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
165pub enum EnvironmentPreference {
166 #[default]
168 OnlyVirtual,
169 ExplicitSystem,
171 OnlySystem,
173 Any,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Default)]
178pub(crate) struct DiscoveryPreferences {
179 python_preference: PythonPreference,
180 environment_preference: EnvironmentPreference,
181}
182
183#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
184pub enum PythonVariant {
185 #[default]
186 Default,
187 Debug,
188 Freethreaded,
189 FreethreadedDebug,
190 Gil,
191 GilDebug,
192}
193
194#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
196pub enum VersionRequest {
197 #[default]
199 Default,
200 Any,
202 Major(u8, PythonVariant),
203 MajorMinor(u8, u8, PythonVariant),
204 MajorMinorPatch(u8, u8, u8, PythonVariant),
205 MajorMinorPrerelease(u8, u8, Prerelease, PythonVariant),
206 MajorMinorPatchPrerelease(u8, u8, u8, Prerelease, PythonVariant),
207 Range(VersionSpecifiers, PythonVariant),
208}
209
210type FindPythonResult = Result<PythonInstallation, PythonNotFound>;
214
215#[derive(Clone, Debug, Error)]
219pub struct PythonNotFound {
220 pub request: PythonRequest,
221 pub python_preference: PythonPreference,
222 pub environment_preference: EnvironmentPreference,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)]
227pub enum PythonSource {
228 ProvidedPath,
230 ActiveEnvironment,
232 CondaPrefix,
234 BaseCondaPrefix,
236 DiscoveredEnvironment,
238 SearchPath,
240 SearchPathFirst,
242 Registry,
244 MicrosoftStore,
246 Managed,
248 ParentInterpreter,
250}
251
252#[derive(Error, Debug)]
253pub enum Error {
254 #[error(transparent)]
255 Io(#[from] io::Error),
256
257 #[error("Failed to inspect Python interpreter from {} at `{}` ", _2, _1.user_display())]
259 Query(
260 #[source] Box<crate::interpreter::Error>,
261 PathBuf,
262 PythonSource,
263 ),
264
265 #[error("Failed to discover managed Python installations")]
268 ManagedPython(#[from] crate::managed::Error),
269
270 #[error(transparent)]
272 VirtualEnv(#[from] crate::virtualenv::Error),
273
274 #[cfg(windows)]
275 #[error("Failed to query installed Python versions from the Windows registry")]
276 RegistryError(#[from] windows::core::Error),
277
278 #[error(transparent)]
279 InvalidEnvironmentVariable(#[from] uv_static::InvalidEnvironmentVariable),
280
281 #[error("Invalid version request: {0}")]
283 InvalidVersionRequest(String),
284
285 #[error("Requesting the 'latest' Python version is not yet supported")]
287 LatestVersionRequest,
288
289 #[error("Interpreter discovery for `{0}` requires `{1}` but only `{2}` is allowed")]
291 SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
292
293 #[error(transparent)]
294 BuildVersion(#[from] crate::python_version::BuildVersionError),
295}
296
297fn python_executables_from_virtual_environments<'a>(
306 preview: Preview,
307) -> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a {
308 let from_active_environment = iter::once_with(|| {
309 virtualenv_from_env()
310 .into_iter()
311 .map(virtualenv_python_executable)
312 .map(|path| Ok((PythonSource::ActiveEnvironment, path)))
313 })
314 .flatten();
315
316 let from_conda_environment = iter::once_with(move || {
318 conda_environment_from_env(CondaEnvironmentKind::Child, preview)
319 .into_iter()
320 .map(virtualenv_python_executable)
321 .map(|path| Ok((PythonSource::CondaPrefix, path)))
322 })
323 .flatten();
324
325 let from_discovered_environment = iter::once_with(|| {
326 virtualenv_from_working_dir()
327 .map(|path| {
328 path.map(virtualenv_python_executable)
329 .map(|path| (PythonSource::DiscoveredEnvironment, path))
330 .into_iter()
331 })
332 .map_err(Error::from)
333 })
334 .flatten_ok();
335
336 from_active_environment
337 .chain(from_conda_environment)
338 .chain(from_discovered_environment)
339}
340
341fn python_executables_from_installed<'a>(
360 version: &'a VersionRequest,
361 implementation: Option<&'a ImplementationName>,
362 platform: PlatformRequest,
363 preference: PythonPreference,
364) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
365 let from_managed_installations = iter::once_with(move || {
366 ManagedPythonInstallations::from_settings(None)
367 .map_err(Error::from)
368 .and_then(|installed_installations| {
369 debug!(
370 "Searching for managed installations at `{}`",
371 installed_installations.root().user_display()
372 );
373 let installations = installed_installations.find_matching_current_platform()?;
374
375 let build_versions = python_build_versions_from_env()?;
376
377 Ok(installations
380 .into_iter()
381 .filter(move |installation| {
382 if !version.matches_version(&installation.version()) {
383 debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`");
384 return false;
385 }
386 if !platform.matches(installation.platform()) {
387 debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
388 return false;
389 }
390
391 if let Some(requested_build) = build_versions.get(&installation.implementation()) {
392 let Some(installation_build) = installation.build() else {
393 debug!(
394 "Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation"
395 );
396 return false;
397 };
398 if installation_build != requested_build {
399 debug!(
400 "Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`"
401 );
402 return false;
403 }
404 }
405
406 true
407 })
408 .inspect(|installation| debug!("Found managed installation `{installation}`"))
409 .map(move |installation| {
410 let executable = version
413 .patch()
414 .is_none()
415 .then(|| {
416 PythonMinorVersionLink::from_installation(
417 &installation,
418 )
419 .filter(PythonMinorVersionLink::exists)
420 .map(
421 |minor_version_link| {
422 minor_version_link.symlink_executable.clone()
423 },
424 )
425 })
426 .flatten()
427 .unwrap_or_else(|| installation.executable(false));
428 (PythonSource::Managed, executable)
429 })
430 )
431 })
432 })
433 .flatten_ok();
434
435 let from_search_path = iter::once_with(move || {
436 python_executables_from_search_path(version, implementation)
437 .enumerate()
438 .map(|(i, path)| {
439 if i == 0 {
440 Ok((PythonSource::SearchPathFirst, path))
441 } else {
442 Ok((PythonSource::SearchPath, path))
443 }
444 })
445 })
446 .flatten();
447
448 #[cfg(windows)]
449 let from_windows_registry: Box<
450 dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
451 > = match uv_static::parse_boolish_environment_variable(EnvVars::UV_PYTHON_NO_REGISTRY) {
452 Ok(Some(true)) => Box::new(iter::empty()),
453 Ok(Some(false) | None) => Box::new(
454 iter::once_with(move || {
455 let version_filter = move |entry: &WindowsPython| {
457 if let Some(found) = &entry.version {
458 if found.string.chars().filter(|c| *c == '.').count() == 1 {
460 version.matches_major_minor(found.major(), found.minor())
461 } else {
462 version.matches_version(found)
463 }
464 } else {
465 true
466 }
467 };
468
469 registry_pythons()
470 .map(|entries| {
471 entries
472 .into_iter()
473 .filter(version_filter)
474 .map(|entry| (PythonSource::Registry, entry.path))
475 .chain(
476 find_microsoft_store_pythons()
477 .filter(version_filter)
478 .map(|entry| (PythonSource::MicrosoftStore, entry.path)),
479 )
480 })
481 .map_err(Error::from)
482 })
483 .flatten_ok(),
484 ),
485 Err(err) => Box::new(iter::once(Err(Error::from(err)))),
486 };
487
488 #[cfg(not(windows))]
489 let from_windows_registry: Box<
490 dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
491 > = Box::new(iter::empty());
492
493 match preference {
494 PythonPreference::OnlyManaged => {
495 if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
499 Box::new(from_managed_installations.chain(from_search_path))
500 } else {
501 Box::new(from_managed_installations)
502 }
503 }
504 PythonPreference::Managed => Box::new(
505 from_managed_installations
506 .chain(from_search_path)
507 .chain(from_windows_registry),
508 ),
509 PythonPreference::System => Box::new(
510 from_search_path
511 .chain(from_windows_registry)
512 .chain(from_managed_installations),
513 ),
514 PythonPreference::OnlySystem => Box::new(from_search_path.chain(from_windows_registry)),
515 }
516}
517
518fn python_executables<'a>(
528 version: &'a VersionRequest,
529 implementation: Option<&'a ImplementationName>,
530 platform: PlatformRequest,
531 environments: EnvironmentPreference,
532 preference: PythonPreference,
533 preview: Preview,
534) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
535 let from_parent_interpreter = iter::once_with(|| {
537 env::var_os(EnvVars::UV_INTERNAL__PARENT_INTERPRETER)
538 .into_iter()
539 .map(|path| Ok((PythonSource::ParentInterpreter, PathBuf::from(path))))
540 })
541 .flatten();
542
543 let from_base_conda_environment = iter::once_with(move || {
545 conda_environment_from_env(CondaEnvironmentKind::Base, preview)
546 .into_iter()
547 .map(virtualenv_python_executable)
548 .map(|path| Ok((PythonSource::BaseCondaPrefix, path)))
549 })
550 .flatten();
551
552 let from_virtual_environments = python_executables_from_virtual_environments(preview);
553 let from_installed =
554 python_executables_from_installed(version, implementation, platform, preference);
555
556 match environments {
560 EnvironmentPreference::OnlyVirtual => {
561 Box::new(from_parent_interpreter.chain(from_virtual_environments))
562 }
563 EnvironmentPreference::ExplicitSystem | EnvironmentPreference::Any => Box::new(
564 from_parent_interpreter
565 .chain(from_virtual_environments)
566 .chain(from_base_conda_environment)
567 .chain(from_installed),
568 ),
569 EnvironmentPreference::OnlySystem => Box::new(
570 from_parent_interpreter
571 .chain(from_base_conda_environment)
572 .chain(from_installed),
573 ),
574 }
575}
576
577fn python_executables_from_search_path<'a>(
589 version: &'a VersionRequest,
590 implementation: Option<&'a ImplementationName>,
591) -> impl Iterator<Item = PathBuf> + 'a {
592 let search_path = env::var_os(EnvVars::UV_PYTHON_SEARCH_PATH)
594 .unwrap_or(env::var_os(EnvVars::PATH).unwrap_or_default());
595
596 let possible_names: Vec<_> = version
597 .executable_names(implementation)
598 .into_iter()
599 .map(|name| name.to_string())
600 .collect();
601
602 trace!(
603 "Searching PATH for executables: {}",
604 possible_names.join(", ")
605 );
606
607 let search_dirs: Vec<_> = env::split_paths(&search_path).collect();
611 let mut seen_dirs = FxHashSet::with_capacity_and_hasher(search_dirs.len(), FxBuildHasher);
612 search_dirs
613 .into_iter()
614 .filter(|dir| dir.is_dir())
615 .flat_map(move |dir| {
616 let dir_clone = dir.clone();
618 trace!(
619 "Checking `PATH` directory for interpreters: {}",
620 dir.display()
621 );
622 same_file::Handle::from_path(&dir)
623 .map(|handle| seen_dirs.insert(handle))
626 .inspect(|fresh_dir| {
627 if !fresh_dir {
628 trace!("Skipping already seen directory: {}", dir.display());
629 }
630 })
631 .unwrap_or(true)
633 .then(|| {
634 possible_names
635 .clone()
636 .into_iter()
637 .flat_map(move |name| {
638 which::which_in_global(&*name, Some(&dir))
640 .into_iter()
641 .flatten()
642 .collect::<Vec<_>>()
645 })
646 .chain(find_all_minor(implementation, version, &dir_clone))
647 .filter(|path| !is_windows_store_shim(path))
648 .inspect(|path| {
649 trace!("Found possible Python executable: {}", path.display());
650 })
651 .chain(
652 cfg!(windows)
654 .then(move || {
655 which::which_in_global("python.bat", Some(&dir_clone))
656 .into_iter()
657 .flatten()
658 .collect::<Vec<_>>()
659 })
660 .into_iter()
661 .flatten(),
662 )
663 })
664 .into_iter()
665 .flatten()
666 })
667}
668
669fn find_all_minor(
674 implementation: Option<&ImplementationName>,
675 version_request: &VersionRequest,
676 dir: &Path,
677) -> impl Iterator<Item = PathBuf> + use<> {
678 match version_request {
679 &VersionRequest::Any
680 | VersionRequest::Default
681 | VersionRequest::Major(_, _)
682 | VersionRequest::Range(_, _) => {
683 let regex = if let Some(implementation) = implementation {
684 Regex::new(&format!(
685 r"^({}|python3)\.(?<minor>\d\d?)t?{}$",
686 regex::escape(&implementation.to_string()),
687 regex::escape(EXE_SUFFIX)
688 ))
689 .unwrap()
690 } else {
691 Regex::new(&format!(
692 r"^python3\.(?<minor>\d\d?)t?{}$",
693 regex::escape(EXE_SUFFIX)
694 ))
695 .unwrap()
696 };
697 let all_minors = fs_err::read_dir(dir)
698 .into_iter()
699 .flatten()
700 .flatten()
701 .map(|entry| entry.path())
702 .filter(move |path| {
703 let Some(filename) = path.file_name() else {
704 return false;
705 };
706 let Some(filename) = filename.to_str() else {
707 return false;
708 };
709 let Some(captures) = regex.captures(filename) else {
710 return false;
711 };
712
713 let minor = captures["minor"].parse().ok();
715 if let Some(minor) = minor {
716 if minor < 6 {
718 return false;
719 }
720 if !version_request.matches_major_minor(3, minor) {
722 return false;
723 }
724 }
725 true
726 })
727 .filter(|path| is_executable(path))
728 .collect::<Vec<_>>();
729 Either::Left(all_minors.into_iter())
730 }
731 VersionRequest::MajorMinor(_, _, _)
732 | VersionRequest::MajorMinorPatch(_, _, _, _)
733 | VersionRequest::MajorMinorPrerelease(_, _, _, _)
734 | VersionRequest::MajorMinorPatchPrerelease(_, _, _, _, _) => Either::Right(iter::empty()),
735 }
736}
737
738fn python_installations<'a>(
748 version: &'a VersionRequest,
749 implementation: Option<&'a ImplementationName>,
750 platform: PlatformRequest,
751 environments: EnvironmentPreference,
752 preference: PythonPreference,
753 cache: &'a Cache,
754 preview: Preview,
755) -> impl Iterator<Item = Result<PythonInstallation, Error>> + 'a {
756 let installations = python_installations_from_executables(
757 python_executables(
761 version,
762 implementation,
763 platform,
764 environments,
765 preference,
766 preview,
767 )
768 .filter_ok(move |(source, path)| {
769 source_satisfies_environment_preference(*source, path, environments)
770 }),
771 cache,
772 )
773 .filter_ok(move |installation| {
774 interpreter_satisfies_environment_preference(
775 installation.source,
776 &installation.interpreter,
777 environments,
778 )
779 })
780 .filter_ok(move |installation| {
781 let request = version.clone().into_request_for_source(installation.source);
782 if request.matches_interpreter(&installation.interpreter) {
783 true
784 } else {
785 debug!(
786 "Skipping interpreter at `{}` from {}: does not satisfy request `{request}`",
787 installation.interpreter.sys_executable().user_display(),
788 installation.source,
789 );
790 false
791 }
792 })
793 .filter_ok(move |installation| preference.allows_installation(installation));
794
795 if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
796 Either::Left(installations.map_ok(|mut installation| {
797 if installation.interpreter.is_managed() {
800 installation.source = PythonSource::Managed;
801 }
802 installation
803 }))
804 } else {
805 Either::Right(installations)
806 }
807}
808
809fn python_installations_from_executables<'a>(
811 executables: impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
812 cache: &'a Cache,
813) -> impl Iterator<Item = Result<PythonInstallation, Error>> + 'a {
814 executables.map(|result| match result {
815 Ok((source, path)) => Interpreter::query(&path, cache)
816 .map(|interpreter| PythonInstallation {
817 source,
818 interpreter,
819 })
820 .inspect(|installation| {
821 debug!(
822 "Found `{}` at `{}` ({source})",
823 installation.key(),
824 path.display()
825 );
826 })
827 .map_err(|err| Error::Query(Box::new(err), path, source))
828 .inspect_err(|err| debug!("{err}")),
829 Err(err) => Err(err),
830 })
831}
832
833fn interpreter_satisfies_environment_preference(
840 source: PythonSource,
841 interpreter: &Interpreter,
842 preference: EnvironmentPreference,
843) -> bool {
844 match (
845 preference,
846 interpreter.is_virtualenv() || (matches!(source, PythonSource::CondaPrefix)),
848 ) {
849 (EnvironmentPreference::Any, _) => true,
850 (EnvironmentPreference::OnlyVirtual, true) => true,
851 (EnvironmentPreference::OnlyVirtual, false) => {
852 debug!(
853 "Ignoring Python interpreter at `{}`: only virtual environments allowed",
854 interpreter.sys_executable().display()
855 );
856 false
857 }
858 (EnvironmentPreference::ExplicitSystem, true) => true,
859 (EnvironmentPreference::ExplicitSystem, false) => {
860 if matches!(
861 source,
862 PythonSource::ProvidedPath | PythonSource::ParentInterpreter
863 ) {
864 debug!(
865 "Allowing explicitly requested system Python interpreter at `{}`",
866 interpreter.sys_executable().display()
867 );
868 true
869 } else {
870 debug!(
871 "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
872 interpreter.sys_executable().display()
873 );
874 false
875 }
876 }
877 (EnvironmentPreference::OnlySystem, true) => {
878 debug!(
879 "Ignoring Python interpreter at `{}`: system interpreter required",
880 interpreter.sys_executable().display()
881 );
882 false
883 }
884 (EnvironmentPreference::OnlySystem, false) => true,
885 }
886}
887
888fn source_satisfies_environment_preference(
895 source: PythonSource,
896 interpreter_path: &Path,
897 preference: EnvironmentPreference,
898) -> bool {
899 match preference {
900 EnvironmentPreference::Any => true,
901 EnvironmentPreference::OnlyVirtual => {
902 if source.is_maybe_virtualenv() {
903 true
904 } else {
905 debug!(
906 "Ignoring Python interpreter at `{}`: only virtual environments allowed",
907 interpreter_path.display()
908 );
909 false
910 }
911 }
912 EnvironmentPreference::ExplicitSystem => {
913 if source.is_maybe_virtualenv() {
914 true
915 } else {
916 debug!(
917 "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
918 interpreter_path.display()
919 );
920 false
921 }
922 }
923 EnvironmentPreference::OnlySystem => {
924 if source.is_maybe_system() {
925 true
926 } else {
927 debug!(
928 "Ignoring Python interpreter at `{}`: system interpreter required",
929 interpreter_path.display()
930 );
931 false
932 }
933 }
934 }
935}
936
937impl Error {
941 pub fn is_critical(&self) -> bool {
942 match self {
943 Self::Query(err, _, source) => match &**err {
946 InterpreterError::Encode(_)
947 | InterpreterError::Io(_)
948 | InterpreterError::SpawnFailed { .. } => true,
949 InterpreterError::UnexpectedResponse(UnexpectedResponseError { path, .. })
950 | InterpreterError::StatusCode(StatusCodeError { path, .. }) => {
951 debug!(
952 "Skipping bad interpreter at {} from {source}: {err}",
953 path.display()
954 );
955 false
956 }
957 InterpreterError::QueryScript { path, err } => {
958 debug!(
959 "Skipping bad interpreter at {} from {source}: {err}",
960 path.display()
961 );
962 false
963 }
964 #[cfg(windows)]
965 InterpreterError::CorruptWindowsPackage { path, err } => {
966 debug!(
967 "Skipping bad interpreter at {} from {source}: {err}",
968 path.display()
969 );
970 false
971 }
972 InterpreterError::PermissionDenied { path, err } => {
973 debug!(
974 "Skipping unexecutable interpreter at {} from {source}: {err}",
975 path.display()
976 );
977 false
978 }
979 InterpreterError::NotFound(path)
980 | InterpreterError::BrokenLink(BrokenLink { path, .. }) => {
981 if matches!(source, PythonSource::ActiveEnvironment)
984 && uv_fs::is_virtualenv_executable(path)
985 {
986 true
987 } else {
988 trace!("Skipping missing interpreter at {}", path.display());
989 false
990 }
991 }
992 },
993 Self::VirtualEnv(VirtualEnvError::MissingPyVenvCfg(path)) => {
994 trace!("Skipping broken virtualenv at {}", path.display());
995 false
996 }
997 _ => true,
998 }
999 }
1000}
1001
1002fn python_installation_from_executable(
1004 path: &PathBuf,
1005 cache: &Cache,
1006) -> Result<PythonInstallation, crate::interpreter::Error> {
1007 Ok(PythonInstallation {
1008 source: PythonSource::ProvidedPath,
1009 interpreter: Interpreter::query(path, cache)?,
1010 })
1011}
1012
1013fn python_installation_from_directory(
1015 path: &PathBuf,
1016 cache: &Cache,
1017) -> Result<PythonInstallation, crate::interpreter::Error> {
1018 let executable = virtualenv_python_executable(path);
1019 python_installation_from_executable(&executable, cache)
1020}
1021
1022fn python_installations_with_executable_name<'a>(
1024 name: &'a str,
1025 cache: &'a Cache,
1026) -> impl Iterator<Item = Result<PythonInstallation, Error>> + 'a {
1027 python_installations_from_executables(
1028 which_all(name)
1029 .into_iter()
1030 .flat_map(|inner| inner.map(|path| Ok((PythonSource::SearchPath, path)))),
1031 cache,
1032 )
1033}
1034
1035pub fn find_python_installations<'a>(
1037 request: &'a PythonRequest,
1038 environments: EnvironmentPreference,
1039 preference: PythonPreference,
1040 cache: &'a Cache,
1041 preview: Preview,
1042) -> Box<dyn Iterator<Item = Result<FindPythonResult, Error>> + 'a> {
1043 let sources = DiscoveryPreferences {
1044 python_preference: preference,
1045 environment_preference: environments,
1046 }
1047 .sources(request);
1048
1049 match request {
1050 PythonRequest::File(path) => Box::new(iter::once({
1051 if preference.allows_source(PythonSource::ProvidedPath) {
1052 debug!("Checking for Python interpreter at {request}");
1053 match python_installation_from_executable(path, cache) {
1054 Ok(installation) => Ok(Ok(installation)),
1055 Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1056 Ok(Err(PythonNotFound {
1057 request: request.clone(),
1058 python_preference: preference,
1059 environment_preference: environments,
1060 }))
1061 }
1062 Err(err) => Err(Error::Query(
1063 Box::new(err),
1064 path.clone(),
1065 PythonSource::ProvidedPath,
1066 )),
1067 }
1068 } else {
1069 Err(Error::SourceNotAllowed(
1070 request.clone(),
1071 PythonSource::ProvidedPath,
1072 preference,
1073 ))
1074 }
1075 })),
1076 PythonRequest::Directory(path) => Box::new(iter::once({
1077 if preference.allows_source(PythonSource::ProvidedPath) {
1078 debug!("Checking for Python interpreter in {request}");
1079 match python_installation_from_directory(path, cache) {
1080 Ok(installation) => Ok(Ok(installation)),
1081 Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1082 Ok(Err(PythonNotFound {
1083 request: request.clone(),
1084 python_preference: preference,
1085 environment_preference: environments,
1086 }))
1087 }
1088 Err(err) => Err(Error::Query(
1089 Box::new(err),
1090 path.clone(),
1091 PythonSource::ProvidedPath,
1092 )),
1093 }
1094 } else {
1095 Err(Error::SourceNotAllowed(
1096 request.clone(),
1097 PythonSource::ProvidedPath,
1098 preference,
1099 ))
1100 }
1101 })),
1102 PythonRequest::ExecutableName(name) => {
1103 if preference.allows_source(PythonSource::SearchPath) {
1104 debug!("Searching for Python interpreter with {request}");
1105 Box::new(
1106 python_installations_with_executable_name(name, cache)
1107 .filter_ok(move |installation| {
1108 interpreter_satisfies_environment_preference(
1109 installation.source,
1110 &installation.interpreter,
1111 environments,
1112 )
1113 })
1114 .map_ok(Ok),
1115 )
1116 } else {
1117 Box::new(iter::once(Err(Error::SourceNotAllowed(
1118 request.clone(),
1119 PythonSource::SearchPath,
1120 preference,
1121 ))))
1122 }
1123 }
1124 PythonRequest::Any => Box::new({
1125 debug!("Searching for any Python interpreter in {sources}");
1126 python_installations(
1127 &VersionRequest::Any,
1128 None,
1129 PlatformRequest::default(),
1130 environments,
1131 preference,
1132 cache,
1133 preview,
1134 )
1135 .map_ok(Ok)
1136 }),
1137 PythonRequest::Default => Box::new({
1138 debug!("Searching for default Python interpreter in {sources}");
1139 python_installations(
1140 &VersionRequest::Default,
1141 None,
1142 PlatformRequest::default(),
1143 environments,
1144 preference,
1145 cache,
1146 preview,
1147 )
1148 .map_ok(Ok)
1149 }),
1150 PythonRequest::Version(version) => {
1151 if let Err(err) = version.check_supported() {
1152 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1153 }
1154 Box::new({
1155 debug!("Searching for {request} in {sources}");
1156 python_installations(
1157 version,
1158 None,
1159 PlatformRequest::default(),
1160 environments,
1161 preference,
1162 cache,
1163 preview,
1164 )
1165 .map_ok(Ok)
1166 })
1167 }
1168 PythonRequest::Implementation(implementation) => Box::new({
1169 debug!("Searching for a {request} interpreter in {sources}");
1170 python_installations(
1171 &VersionRequest::Default,
1172 Some(implementation),
1173 PlatformRequest::default(),
1174 environments,
1175 preference,
1176 cache,
1177 preview,
1178 )
1179 .filter_ok(|installation| implementation.matches_interpreter(&installation.interpreter))
1180 .map_ok(Ok)
1181 }),
1182 PythonRequest::ImplementationVersion(implementation, version) => {
1183 if let Err(err) = version.check_supported() {
1184 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1185 }
1186 Box::new({
1187 debug!("Searching for {request} in {sources}");
1188 python_installations(
1189 version,
1190 Some(implementation),
1191 PlatformRequest::default(),
1192 environments,
1193 preference,
1194 cache,
1195 preview,
1196 )
1197 .filter_ok(|installation| {
1198 implementation.matches_interpreter(&installation.interpreter)
1199 })
1200 .map_ok(Ok)
1201 })
1202 }
1203 PythonRequest::Key(request) => {
1204 if let Some(version) = request.version() {
1205 if let Err(err) = version.check_supported() {
1206 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1207 }
1208 }
1209
1210 Box::new({
1211 debug!("Searching for {request} in {sources}");
1212 python_installations(
1213 request.version().unwrap_or(&VersionRequest::Default),
1214 request.implementation(),
1215 request.platform(),
1216 environments,
1217 preference,
1218 cache,
1219 preview,
1220 )
1221 .filter_ok(move |installation| {
1222 request.satisfied_by_interpreter(&installation.interpreter)
1223 })
1224 .map_ok(Ok)
1225 })
1226 }
1227 }
1228}
1229
1230pub(crate) fn find_python_installation(
1235 request: &PythonRequest,
1236 environments: EnvironmentPreference,
1237 preference: PythonPreference,
1238 cache: &Cache,
1239 preview: Preview,
1240) -> Result<FindPythonResult, Error> {
1241 let installations =
1242 find_python_installations(request, environments, preference, cache, preview);
1243 let mut first_prerelease = None;
1244 let mut first_debug = None;
1245 let mut first_managed = None;
1246 let mut first_error = None;
1247 for result in installations {
1248 if !result.as_ref().err().is_none_or(Error::is_critical) {
1250 if first_error.is_none() {
1252 if let Err(err) = result {
1253 first_error = Some(err);
1254 }
1255 }
1256 continue;
1257 }
1258
1259 let Ok(Ok(ref installation)) = result else {
1261 return result;
1262 };
1263
1264 let has_default_executable_name = installation.interpreter.has_default_executable_name()
1270 && matches!(
1271 installation.source,
1272 PythonSource::SearchPath | PythonSource::SearchPathFirst
1273 );
1274
1275 if installation.python_version().pre().is_some()
1278 && !request.allows_prereleases()
1279 && !installation.source.allows_prereleases()
1280 && !has_default_executable_name
1281 {
1282 debug!("Skipping pre-release installation {}", installation.key());
1283 if first_prerelease.is_none() {
1284 first_prerelease = Some(installation.clone());
1285 }
1286 continue;
1287 }
1288
1289 if installation.key().variant().is_debug()
1292 && !request.allows_debug()
1293 && !installation.source.allows_debug()
1294 && !has_default_executable_name
1295 {
1296 debug!("Skipping debug installation {}", installation.key());
1297 if first_debug.is_none() {
1298 first_debug = Some(installation.clone());
1299 }
1300 continue;
1301 }
1302
1303 if installation.is_alternative_implementation()
1308 && !request.allows_alternative_implementations()
1309 && !installation.source.allows_alternative_implementations()
1310 && !has_default_executable_name
1311 {
1312 debug!("Skipping alternative implementation {}", installation.key());
1313 continue;
1314 }
1315
1316 if matches!(preference, PythonPreference::System) && installation.is_managed() {
1319 debug!(
1320 "Skipping managed installation {}: system installation preferred",
1321 installation.key()
1322 );
1323 if first_managed.is_none() {
1324 first_managed = Some(installation.clone());
1325 }
1326 continue;
1327 }
1328
1329 return result;
1331 }
1332
1333 if let Some(installation) = first_managed {
1336 debug!(
1337 "Allowing managed installation {}: no system installations",
1338 installation.key()
1339 );
1340 return Ok(Ok(installation));
1341 }
1342
1343 if let Some(installation) = first_debug {
1346 debug!(
1347 "Allowing debug installation {}: no non-debug installations",
1348 installation.key()
1349 );
1350 return Ok(Ok(installation));
1351 }
1352
1353 if let Some(installation) = first_prerelease {
1355 debug!(
1356 "Allowing pre-release installation {}: no stable installations",
1357 installation.key()
1358 );
1359 return Ok(Ok(installation));
1360 }
1361
1362 if let Some(err) = first_error {
1365 return Err(err);
1366 }
1367
1368 Ok(Err(PythonNotFound {
1369 request: request.clone(),
1370 environment_preference: environments,
1371 python_preference: preference,
1372 }))
1373}
1374
1375#[instrument(skip_all, fields(request))]
1389pub(crate) async fn find_best_python_installation(
1390 request: &PythonRequest,
1391 environments: EnvironmentPreference,
1392 preference: PythonPreference,
1393 downloads_enabled: bool,
1394 download_list: &ManagedPythonDownloadList,
1395 client: &BaseClient,
1396 retry_policy: &ExponentialBackoff,
1397 cache: &Cache,
1398 reporter: Option<&dyn crate::downloads::Reporter>,
1399 python_install_mirror: Option<&str>,
1400 pypy_install_mirror: Option<&str>,
1401 preview: Preview,
1402) -> Result<PythonInstallation, crate::Error> {
1403 debug!("Starting Python discovery for {request}");
1404 let original_request = request;
1405
1406 let mut previous_fetch_failed = false;
1407
1408 let request_without_patch = match request {
1409 PythonRequest::Version(version) => {
1410 if version.has_patch() {
1411 Some(PythonRequest::Version(version.clone().without_patch()))
1412 } else {
1413 None
1414 }
1415 }
1416 PythonRequest::ImplementationVersion(implementation, version) => Some(
1417 PythonRequest::ImplementationVersion(*implementation, version.clone().without_patch()),
1418 ),
1419 _ => None,
1420 };
1421
1422 for (attempt, request) in iter::once(original_request)
1423 .chain(request_without_patch.iter())
1424 .chain(iter::once(&PythonRequest::Default))
1425 .enumerate()
1426 {
1427 debug!(
1428 "Looking for {request}{}",
1429 if request != original_request {
1430 format!(" attempt {attempt} (fallback after failing to find: {original_request})")
1431 } else {
1432 String::new()
1433 }
1434 );
1435 let result = find_python_installation(request, environments, preference, cache, preview);
1436 let error = match result {
1437 Ok(Ok(installation)) => {
1438 warn_on_unsupported_python(installation.interpreter());
1439 return Ok(installation);
1440 }
1441 Ok(Err(error)) => error.into(),
1443 Err(error) if !error.is_critical() => error.into(),
1444 Err(error) => return Err(error.into()),
1445 };
1446
1447 if downloads_enabled
1449 && !previous_fetch_failed
1450 && let Some(download_request) = PythonDownloadRequest::from_request(request)
1451 {
1452 let download = download_request
1453 .clone()
1454 .fill()
1455 .map(|request| download_list.find(&request));
1456
1457 let result = match download {
1458 Ok(Ok(download)) => PythonInstallation::fetch(
1459 download,
1460 client,
1461 retry_policy,
1462 cache,
1463 reporter,
1464 python_install_mirror,
1465 pypy_install_mirror,
1466 )
1467 .await
1468 .map(Some),
1469 Ok(Err(crate::downloads::Error::NoDownloadFound(_))) => Ok(None),
1470 Ok(Err(error)) => Err(error.into()),
1471 Err(error) => Err(error.into()),
1472 };
1473 if let Ok(Some(installation)) = result {
1474 return Ok(installation);
1475 }
1476 if let Err(error) = result {
1484 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1488 return Err(error);
1489 }
1490
1491 let mut error_chain = String::new();
1492 let error = anyhow::Error::from(error).context(format!(
1494 "A managed Python download is available for {request}, but an error occurred when attempting to download it."
1495 ));
1496 uv_warnings::write_error_chain(
1497 error.as_ref(),
1498 &mut error_chain,
1499 "warning",
1500 AnsiColors::Yellow,
1501 )
1502 .unwrap();
1503 anstream::eprint!("{}", error_chain);
1504 previous_fetch_failed = true;
1505 }
1506 }
1507
1508 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1514 return Err(match error {
1515 crate::Error::MissingPython(err, _) => PythonNotFound {
1516 request: original_request.clone(),
1518 python_preference: err.python_preference,
1519 environment_preference: err.environment_preference,
1520 }
1521 .into(),
1522 other => other,
1523 });
1524 }
1525 }
1526
1527 unreachable!("The loop should have terminated when it reached PythonRequest::Default");
1528}
1529
1530fn warn_on_unsupported_python(interpreter: &Interpreter) {
1532 if interpreter.python_tuple() < (3, 8) {
1534 warn_user_once!(
1535 "uv is only compatible with Python >=3.8, found Python {}",
1536 interpreter.python_version()
1537 );
1538 }
1539}
1540
1541#[cfg(windows)]
1558pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
1559 use std::os::windows::fs::MetadataExt;
1560 use std::os::windows::prelude::OsStrExt;
1561 use windows::Win32::Foundation::CloseHandle;
1562 use windows::Win32::Storage::FileSystem::{
1563 CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
1564 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_MODE, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
1565 OPEN_EXISTING,
1566 };
1567 use windows::Win32::System::IO::DeviceIoControl;
1568 use windows::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
1569 use windows::core::PCWSTR;
1570
1571 if !path.is_absolute() {
1573 return false;
1574 }
1575
1576 let mut components = path.components().rev();
1579
1580 if !components
1582 .next()
1583 .and_then(|component| component.as_os_str().to_str())
1584 .is_some_and(|component| {
1585 component.starts_with("python")
1586 && std::path::Path::new(component)
1587 .extension()
1588 .is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
1589 })
1590 {
1591 return false;
1592 }
1593
1594 if components
1596 .next()
1597 .is_none_or(|component| component.as_os_str() != "WindowsApps")
1598 {
1599 return false;
1600 }
1601
1602 if components
1604 .next()
1605 .is_none_or(|component| component.as_os_str() != "Microsoft")
1606 {
1607 return false;
1608 }
1609
1610 let Ok(md) = fs_err::symlink_metadata(path) else {
1612 return false;
1613 };
1614 if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 {
1615 return false;
1616 }
1617
1618 let mut path_encoded = path
1619 .as_os_str()
1620 .encode_wide()
1621 .chain(std::iter::once(0))
1622 .collect::<Vec<_>>();
1623
1624 #[allow(unsafe_code)]
1626 let reparse_handle = unsafe {
1627 CreateFileW(
1628 PCWSTR(path_encoded.as_mut_ptr()),
1629 0,
1630 FILE_SHARE_MODE(0),
1631 None,
1632 OPEN_EXISTING,
1633 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
1634 None,
1635 )
1636 };
1637
1638 let Ok(reparse_handle) = reparse_handle else {
1639 return false;
1640 };
1641
1642 let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
1643 let mut bytes_returned = 0;
1644
1645 #[allow(unsafe_code, clippy::cast_possible_truncation)]
1647 let success = unsafe {
1648 DeviceIoControl(
1649 reparse_handle,
1650 FSCTL_GET_REPARSE_POINT,
1651 None,
1652 0,
1653 Some(buf.as_mut_ptr().cast()),
1654 buf.len() as u32 * 2,
1655 Some(&raw mut bytes_returned),
1656 None,
1657 )
1658 .is_ok()
1659 };
1660
1661 #[allow(unsafe_code)]
1663 unsafe {
1664 let _ = CloseHandle(reparse_handle);
1665 }
1666
1667 if !success {
1669 return false;
1670 }
1671
1672 let reparse_point = String::from_utf16_lossy(&buf[..bytes_returned as usize]);
1673 reparse_point.contains("\\AppInstallerPythonRedirector.exe")
1674}
1675
1676#[cfg(not(windows))]
1680fn is_windows_store_shim(_path: &Path) -> bool {
1681 false
1682}
1683
1684impl PythonVariant {
1685 fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
1686 match self {
1687 Self::Default => {
1688 if (interpreter.python_major(), interpreter.python_minor()) >= (3, 14) {
1691 true
1694 } else {
1695 !interpreter.gil_disabled()
1698 }
1699 }
1700 Self::Debug => interpreter.debug_enabled(),
1701 Self::Freethreaded => interpreter.gil_disabled(),
1702 Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
1703 Self::Gil => !interpreter.gil_disabled(),
1704 Self::GilDebug => !interpreter.gil_disabled() && interpreter.debug_enabled(),
1705 }
1706 }
1707
1708 pub fn executable_suffix(self) -> &'static str {
1712 match self {
1713 Self::Default => "",
1714 Self::Debug => "d",
1715 Self::Freethreaded => "t",
1716 Self::FreethreadedDebug => "td",
1717 Self::Gil => "",
1718 Self::GilDebug => "d",
1719 }
1720 }
1721
1722 pub fn display_suffix(self) -> &'static str {
1724 match self {
1725 Self::Default => "",
1726 Self::Debug => "+debug",
1727 Self::Freethreaded => "+freethreaded",
1728 Self::FreethreadedDebug => "+freethreaded+debug",
1729 Self::Gil => "+gil",
1730 Self::GilDebug => "+gil+debug",
1731 }
1732 }
1733
1734 pub fn lib_suffix(self) -> &'static str {
1737 match self {
1738 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "",
1739 Self::Freethreaded | Self::FreethreadedDebug => "t",
1740 }
1741 }
1742
1743 pub fn is_freethreaded(self) -> bool {
1744 match self {
1745 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false,
1746 Self::Freethreaded | Self::FreethreadedDebug => true,
1747 }
1748 }
1749
1750 pub fn is_debug(self) -> bool {
1751 match self {
1752 Self::Default | Self::Freethreaded | Self::Gil => false,
1753 Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true,
1754 }
1755 }
1756}
1757impl PythonRequest {
1758 pub fn from_requires_python(requires_python: RequiresPython) -> Option<Self> {
1760 let specifiers = requires_python.into_specifiers();
1761 if specifiers.is_empty() {
1762 return None;
1763 }
1764
1765 Some(Self::Version(VersionRequest::from_specifiers(
1766 specifiers,
1767 PythonVariant::Default,
1768 )))
1769 }
1770
1771 pub fn parse(value: &str) -> Self {
1779 let lowercase_value = &value.to_ascii_lowercase();
1780
1781 if lowercase_value == "any" {
1783 return Self::Any;
1784 }
1785 if lowercase_value == "default" {
1786 return Self::Default;
1787 }
1788
1789 let abstract_version_prefixes = ["python", ""];
1791 let all_implementation_names =
1792 ImplementationName::long_names().chain(ImplementationName::short_names());
1793 if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1796 abstract_version_prefixes,
1797 all_implementation_names,
1798 lowercase_value,
1799 ) {
1800 return request;
1801 }
1802
1803 let value_as_path = PathBuf::from(value);
1804 if value_as_path.is_dir() {
1806 return Self::Directory(value_as_path);
1807 }
1808 if value_as_path.is_file() {
1810 return Self::File(value_as_path);
1811 }
1812
1813 #[cfg(windows)]
1815 if value_as_path.extension().is_none() {
1816 let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1817 if value_as_path.is_file() {
1818 return Self::File(value_as_path);
1819 }
1820 }
1821
1822 #[cfg(test)]
1827 if value_as_path.is_relative() {
1828 if let Ok(current_dir) = crate::current_dir() {
1829 let relative = current_dir.join(&value_as_path);
1830 if relative.is_dir() {
1831 return Self::Directory(relative);
1832 }
1833 if relative.is_file() {
1834 return Self::File(relative);
1835 }
1836 }
1837 }
1838 if value.contains(std::path::MAIN_SEPARATOR) {
1841 return Self::File(value_as_path);
1842 }
1843 if cfg!(windows) && value.contains('/') {
1846 return Self::File(value_as_path);
1847 }
1848 if let Ok(request) = PythonDownloadRequest::from_str(value) {
1849 return Self::Key(request);
1850 }
1851 Self::ExecutableName(value.to_string())
1854 }
1855
1856 pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
1870 let lowercase_value = &value.to_ascii_lowercase();
1871 let abstract_version_prefixes = if cfg!(windows) {
1873 &["python", "pythonw"][..]
1874 } else {
1875 &["python"][..]
1876 };
1877 if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
1879 return Ok(Some(Self::Default));
1880 }
1881 Self::parse_versions_and_implementations(
1882 abstract_version_prefixes.iter().copied(),
1883 ImplementationName::long_names(),
1884 lowercase_value,
1885 )
1886 }
1887
1888 fn parse_versions_and_implementations<'a>(
1897 abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
1899 implementation_names: impl IntoIterator<Item = &'a str>,
1901 lowercase_value: &str,
1903 ) -> Result<Option<Self>, Error> {
1904 for prefix in abstract_version_prefixes {
1905 if let Some(version_request) =
1906 Self::try_split_prefix_and_version(prefix, lowercase_value)?
1907 {
1908 return Ok(Some(Self::Version(version_request)));
1912 }
1913 }
1914 for implementation in implementation_names {
1915 if lowercase_value == implementation {
1916 return Ok(Some(Self::Implementation(
1917 ImplementationName::from_str(implementation).unwrap(),
1920 )));
1921 }
1922 if let Some(version_request) =
1923 Self::try_split_prefix_and_version(implementation, lowercase_value)?
1924 {
1925 return Ok(Some(Self::ImplementationVersion(
1927 ImplementationName::from_str(implementation).unwrap(),
1929 version_request,
1930 )));
1931 }
1932 }
1933 Ok(None)
1934 }
1935
1936 fn try_split_prefix_and_version(
1947 prefix: &str,
1948 lowercase_value: &str,
1949 ) -> Result<Option<VersionRequest>, Error> {
1950 if lowercase_value.starts_with('@') {
1951 return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
1952 }
1953 let Some(rest) = lowercase_value.strip_prefix(prefix) else {
1954 return Ok(None);
1955 };
1956 if rest.is_empty() {
1958 return Ok(None);
1959 }
1960 if let Some(after_at) = rest.strip_prefix('@') {
1963 if after_at == "latest" {
1964 return Err(Error::LatestVersionRequest);
1967 }
1968 return after_at.parse().map(Some);
1969 }
1970 Ok(rest.parse().ok())
1973 }
1974
1975 pub fn includes_patch(&self) -> bool {
1977 match self {
1978 Self::Default => false,
1979 Self::Any => false,
1980 Self::Version(version_request) => version_request.patch().is_some(),
1981 Self::Directory(..) => false,
1982 Self::File(..) => false,
1983 Self::ExecutableName(..) => false,
1984 Self::Implementation(..) => false,
1985 Self::ImplementationVersion(_, version) => version.patch().is_some(),
1986 Self::Key(request) => request
1987 .version
1988 .as_ref()
1989 .is_some_and(|request| request.patch().is_some()),
1990 }
1991 }
1992
1993 pub fn includes_prerelease(&self) -> bool {
1995 match self {
1996 Self::Default => false,
1997 Self::Any => false,
1998 Self::Version(version_request) => version_request.prerelease().is_some(),
1999 Self::Directory(..) => false,
2000 Self::File(..) => false,
2001 Self::ExecutableName(..) => false,
2002 Self::Implementation(..) => false,
2003 Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
2004 Self::Key(request) => request
2005 .version
2006 .as_ref()
2007 .is_some_and(|request| request.prerelease().is_some()),
2008 }
2009 }
2010
2011 pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
2013 fn is_same_executable(path1: &Path, path2: &Path) -> bool {
2015 path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
2016 }
2017
2018 match self {
2019 Self::Default | Self::Any => true,
2020 Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2021 Self::Directory(directory) => {
2022 is_same_executable(directory, interpreter.sys_prefix())
2024 || is_same_executable(
2025 virtualenv_python_executable(directory).as_path(),
2026 interpreter.sys_executable(),
2027 )
2028 }
2029 Self::File(file) => {
2030 if is_same_executable(interpreter.sys_executable(), file) {
2032 return true;
2033 }
2034 if interpreter
2036 .sys_base_executable()
2037 .is_some_and(|sys_base_executable| {
2038 is_same_executable(sys_base_executable, file)
2039 })
2040 {
2041 return true;
2042 }
2043 if cfg!(windows) {
2048 if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2049 if let (Some(file_base), Some(interpreter_base)) = (
2050 file_interpreter.sys_base_executable(),
2051 interpreter.sys_base_executable(),
2052 ) {
2053 if is_same_executable(file_base, interpreter_base) {
2054 return true;
2055 }
2056 }
2057 }
2058 }
2059 false
2060 }
2061 Self::ExecutableName(name) => {
2062 if interpreter
2064 .sys_executable()
2065 .file_name()
2066 .is_some_and(|filename| filename == name.as_str())
2067 {
2068 return true;
2069 }
2070 if interpreter
2072 .sys_base_executable()
2073 .and_then(|executable| executable.file_name())
2074 .is_some_and(|file_name| file_name == name.as_str())
2075 {
2076 return true;
2077 }
2078 if which(name)
2081 .ok()
2082 .as_ref()
2083 .and_then(|executable| executable.file_name())
2084 .is_some_and(|file_name| file_name == name.as_str())
2085 {
2086 return true;
2087 }
2088 false
2089 }
2090 Self::Implementation(implementation) => interpreter
2091 .implementation_name()
2092 .eq_ignore_ascii_case(implementation.into()),
2093 Self::ImplementationVersion(implementation, version) => {
2094 version.matches_interpreter(interpreter)
2095 && interpreter
2096 .implementation_name()
2097 .eq_ignore_ascii_case(implementation.into())
2098 }
2099 Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2100 }
2101 }
2102
2103 pub(crate) fn allows_prereleases(&self) -> bool {
2105 match self {
2106 Self::Default => false,
2107 Self::Any => true,
2108 Self::Version(version) => version.allows_prereleases(),
2109 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2110 Self::Implementation(_) => false,
2111 Self::ImplementationVersion(_, _) => true,
2112 Self::Key(request) => request.allows_prereleases(),
2113 }
2114 }
2115
2116 pub(crate) fn allows_debug(&self) -> bool {
2118 match self {
2119 Self::Default => false,
2120 Self::Any => true,
2121 Self::Version(version) => version.is_debug(),
2122 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2123 Self::Implementation(_) => false,
2124 Self::ImplementationVersion(_, _) => true,
2125 Self::Key(request) => request.allows_debug(),
2126 }
2127 }
2128
2129 pub(crate) fn allows_alternative_implementations(&self) -> bool {
2131 match self {
2132 Self::Default => false,
2133 Self::Any => true,
2134 Self::Version(_) => false,
2135 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2136 Self::Implementation(implementation)
2137 | Self::ImplementationVersion(implementation, _) => {
2138 !matches!(implementation, ImplementationName::CPython)
2139 }
2140 Self::Key(request) => request.allows_alternative_implementations(),
2141 }
2142 }
2143
2144 pub(crate) fn is_explicit_system(&self) -> bool {
2145 matches!(self, Self::File(_) | Self::Directory(_))
2146 }
2147
2148 pub fn to_canonical_string(&self) -> String {
2152 match self {
2153 Self::Any => "any".to_string(),
2154 Self::Default => "default".to_string(),
2155 Self::Version(version) => version.to_string(),
2156 Self::Directory(path) => path.display().to_string(),
2157 Self::File(path) => path.display().to_string(),
2158 Self::ExecutableName(name) => name.clone(),
2159 Self::Implementation(implementation) => implementation.to_string(),
2160 Self::ImplementationVersion(implementation, version) => {
2161 format!("{implementation}@{version}")
2162 }
2163 Self::Key(request) => request.to_string(),
2164 }
2165 }
2166
2167 pub fn as_pep440_version(&self) -> Option<Version> {
2171 match self {
2172 Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2173 Self::Key(download_request) => download_request
2174 .version()
2175 .and_then(VersionRequest::as_pep440_version),
2176 Self::Default
2177 | Self::Any
2178 | Self::Directory(_)
2179 | Self::File(_)
2180 | Self::ExecutableName(_)
2181 | Self::Implementation(_) => None,
2182 }
2183 }
2184
2185 pub fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2191 match self {
2192 Self::Version(version) | Self::ImplementationVersion(_, version) => {
2193 version.as_version_specifiers()
2194 }
2195 Self::Key(download_request) => download_request
2196 .version()
2197 .and_then(VersionRequest::as_version_specifiers),
2198 Self::Default
2199 | Self::Any
2200 | Self::Directory(_)
2201 | Self::File(_)
2202 | Self::ExecutableName(_)
2203 | Self::Implementation(_) => None,
2204 }
2205 }
2206
2207 pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2213 let Some(specifiers) = self.as_version_specifiers() else {
2214 return true;
2215 };
2216
2217 let request_range = release_specifiers_to_ranges(specifiers);
2218 let requires_python_range =
2219 release_specifiers_to_ranges(requires_python.specifiers().clone());
2220 !request_range
2221 .intersection(&requires_python_range)
2222 .is_empty()
2223 }
2224}
2225
2226impl PythonSource {
2227 pub fn is_managed(self) -> bool {
2228 matches!(self, Self::Managed)
2229 }
2230
2231 pub(crate) fn allows_prereleases(self) -> bool {
2233 match self {
2234 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2235 Self::SearchPath
2236 | Self::SearchPathFirst
2237 | Self::CondaPrefix
2238 | Self::BaseCondaPrefix
2239 | Self::ProvidedPath
2240 | Self::ParentInterpreter
2241 | Self::ActiveEnvironment
2242 | Self::DiscoveredEnvironment => true,
2243 }
2244 }
2245
2246 pub(crate) fn allows_debug(self) -> bool {
2248 match self {
2249 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2250 Self::SearchPath
2251 | Self::SearchPathFirst
2252 | Self::CondaPrefix
2253 | Self::BaseCondaPrefix
2254 | Self::ProvidedPath
2255 | Self::ParentInterpreter
2256 | Self::ActiveEnvironment
2257 | Self::DiscoveredEnvironment => true,
2258 }
2259 }
2260
2261 pub(crate) fn allows_alternative_implementations(self) -> bool {
2263 match self {
2264 Self::Managed
2265 | Self::Registry
2266 | Self::SearchPath
2267 | Self::SearchPathFirst
2270 | Self::MicrosoftStore => false,
2271 Self::CondaPrefix
2272 | Self::BaseCondaPrefix
2273 | Self::ProvidedPath
2274 | Self::ParentInterpreter
2275 | Self::ActiveEnvironment
2276 | Self::DiscoveredEnvironment => true,
2277 }
2278 }
2279
2280 pub(crate) fn is_maybe_virtualenv(self) -> bool {
2292 match self {
2293 Self::ProvidedPath
2294 | Self::ActiveEnvironment
2295 | Self::DiscoveredEnvironment
2296 | Self::CondaPrefix
2297 | Self::BaseCondaPrefix
2298 | Self::ParentInterpreter
2299 | Self::SearchPathFirst => true,
2300 Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2301 }
2302 }
2303
2304 pub(crate) fn is_explicit(self) -> bool {
2307 match self {
2308 Self::ProvidedPath
2309 | Self::ParentInterpreter
2310 | Self::ActiveEnvironment
2311 | Self::CondaPrefix => true,
2312 Self::Managed
2313 | Self::DiscoveredEnvironment
2314 | Self::SearchPath
2315 | Self::SearchPathFirst
2316 | Self::Registry
2317 | Self::MicrosoftStore
2318 | Self::BaseCondaPrefix => false,
2319 }
2320 }
2321
2322 pub(crate) fn is_maybe_system(self) -> bool {
2324 match self {
2325 Self::CondaPrefix
2326 | Self::BaseCondaPrefix
2327 | Self::ParentInterpreter
2328 | Self::ProvidedPath
2329 | Self::Managed
2330 | Self::SearchPath
2331 | Self::SearchPathFirst
2332 | Self::Registry
2333 | Self::MicrosoftStore => true,
2334 Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2335 }
2336 }
2337}
2338
2339impl PythonPreference {
2340 fn allows_source(self, source: PythonSource) -> bool {
2341 if !matches!(
2343 source,
2344 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2345 ) {
2346 return true;
2347 }
2348
2349 match self {
2350 Self::OnlyManaged => matches!(source, PythonSource::Managed),
2351 Self::Managed | Self::System => matches!(
2352 source,
2353 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2354 ),
2355 Self::OnlySystem => {
2356 matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2357 }
2358 }
2359 }
2360
2361 pub(crate) fn allows_managed(self) -> bool {
2362 match self {
2363 Self::OnlySystem => false,
2364 Self::Managed | Self::System | Self::OnlyManaged => true,
2365 }
2366 }
2367
2368 pub fn allows_interpreter(self, interpreter: &Interpreter) -> bool {
2373 match self {
2374 Self::OnlyManaged => interpreter.is_managed(),
2375 Self::OnlySystem => !interpreter.is_managed(),
2376 Self::Managed | Self::System => true,
2377 }
2378 }
2379
2380 pub fn allows_installation(self, installation: &PythonInstallation) -> bool {
2388 let source = installation.source;
2389 let interpreter = &installation.interpreter;
2390
2391 match self {
2392 Self::OnlyManaged => {
2393 if self.allows_interpreter(interpreter) {
2394 true
2395 } else if source.is_explicit() {
2396 debug!(
2397 "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2398 interpreter.sys_executable().display()
2399 );
2400 true
2401 } else {
2402 debug!(
2403 "Ignoring Python interpreter at `{}`: only managed interpreters allowed",
2404 interpreter.sys_executable().display()
2405 );
2406 false
2407 }
2408 }
2409 Self::Managed | Self::System => true,
2411 Self::OnlySystem => {
2412 if self.allows_interpreter(interpreter) {
2413 true
2414 } else if source.is_explicit() {
2415 debug!(
2416 "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2417 interpreter.sys_executable().display()
2418 );
2419 true
2420 } else {
2421 debug!(
2422 "Ignoring Python interpreter at `{}`: only system interpreters allowed",
2423 interpreter.sys_executable().display()
2424 );
2425 false
2426 }
2427 }
2428 }
2429 }
2430
2431 #[must_use]
2436 pub fn with_system_flag(self, system: bool) -> Self {
2437 match self {
2438 Self::OnlyManaged => self,
2443 Self::Managed => {
2444 if system {
2445 Self::System
2446 } else {
2447 self
2448 }
2449 }
2450 Self::System => self,
2451 Self::OnlySystem => self,
2452 }
2453 }
2454}
2455
2456impl PythonDownloads {
2457 pub fn is_automatic(self) -> bool {
2458 matches!(self, Self::Automatic)
2459 }
2460}
2461
2462impl EnvironmentPreference {
2463 pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2464 match (system, mutable) {
2465 (true, _) => Self::OnlySystem,
2467 (false, true) => Self::ExplicitSystem,
2469 (false, false) => Self::Any,
2471 }
2472 }
2473}
2474
2475#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2476pub(crate) struct ExecutableName {
2477 implementation: Option<ImplementationName>,
2478 major: Option<u8>,
2479 minor: Option<u8>,
2480 patch: Option<u8>,
2481 prerelease: Option<Prerelease>,
2482 variant: PythonVariant,
2483}
2484
2485#[derive(Debug, Clone, PartialEq, Eq)]
2486struct ExecutableNameComparator<'a> {
2487 name: ExecutableName,
2488 request: &'a VersionRequest,
2489 implementation: Option<&'a ImplementationName>,
2490}
2491
2492impl Ord for ExecutableNameComparator<'_> {
2493 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2497 let name_ordering = if self.implementation.is_some() {
2500 std::cmp::Ordering::Greater
2501 } else {
2502 std::cmp::Ordering::Less
2503 };
2504 if self.name.implementation.is_none() && other.name.implementation.is_some() {
2505 return name_ordering.reverse();
2506 }
2507 if self.name.implementation.is_some() && other.name.implementation.is_none() {
2508 return name_ordering;
2509 }
2510 let ordering = self.name.implementation.cmp(&other.name.implementation);
2512 if ordering != std::cmp::Ordering::Equal {
2513 return ordering;
2514 }
2515 let ordering = self.name.major.cmp(&other.name.major);
2516 let is_default_request =
2517 matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2518 if ordering != std::cmp::Ordering::Equal {
2519 return if is_default_request {
2520 ordering.reverse()
2521 } else {
2522 ordering
2523 };
2524 }
2525 let ordering = self.name.minor.cmp(&other.name.minor);
2526 if ordering != std::cmp::Ordering::Equal {
2527 return if is_default_request {
2528 ordering.reverse()
2529 } else {
2530 ordering
2531 };
2532 }
2533 let ordering = self.name.patch.cmp(&other.name.patch);
2534 if ordering != std::cmp::Ordering::Equal {
2535 return if is_default_request {
2536 ordering.reverse()
2537 } else {
2538 ordering
2539 };
2540 }
2541 let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2542 if ordering != std::cmp::Ordering::Equal {
2543 return if is_default_request {
2544 ordering.reverse()
2545 } else {
2546 ordering
2547 };
2548 }
2549 let ordering = self.name.variant.cmp(&other.name.variant);
2550 if ordering != std::cmp::Ordering::Equal {
2551 return if is_default_request {
2552 ordering.reverse()
2553 } else {
2554 ordering
2555 };
2556 }
2557 ordering
2558 }
2559}
2560
2561impl PartialOrd for ExecutableNameComparator<'_> {
2562 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2563 Some(self.cmp(other))
2564 }
2565}
2566
2567impl ExecutableName {
2568 #[must_use]
2569 fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2570 self.implementation = Some(implementation);
2571 self
2572 }
2573
2574 #[must_use]
2575 fn with_major(mut self, major: u8) -> Self {
2576 self.major = Some(major);
2577 self
2578 }
2579
2580 #[must_use]
2581 fn with_minor(mut self, minor: u8) -> Self {
2582 self.minor = Some(minor);
2583 self
2584 }
2585
2586 #[must_use]
2587 fn with_patch(mut self, patch: u8) -> Self {
2588 self.patch = Some(patch);
2589 self
2590 }
2591
2592 #[must_use]
2593 fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2594 self.prerelease = Some(prerelease);
2595 self
2596 }
2597
2598 #[must_use]
2599 fn with_variant(mut self, variant: PythonVariant) -> Self {
2600 self.variant = variant;
2601 self
2602 }
2603
2604 fn into_comparator<'a>(
2605 self,
2606 request: &'a VersionRequest,
2607 implementation: Option<&'a ImplementationName>,
2608 ) -> ExecutableNameComparator<'a> {
2609 ExecutableNameComparator {
2610 name: self,
2611 request,
2612 implementation,
2613 }
2614 }
2615}
2616
2617impl fmt::Display for ExecutableName {
2618 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2619 if let Some(implementation) = self.implementation {
2620 write!(f, "{implementation}")?;
2621 } else {
2622 f.write_str("python")?;
2623 }
2624 if let Some(major) = self.major {
2625 write!(f, "{major}")?;
2626 if let Some(minor) = self.minor {
2627 write!(f, ".{minor}")?;
2628 if let Some(patch) = self.patch {
2629 write!(f, ".{patch}")?;
2630 }
2631 }
2632 }
2633 if let Some(prerelease) = &self.prerelease {
2634 write!(f, "{prerelease}")?;
2635 }
2636 f.write_str(self.variant.executable_suffix())?;
2637 f.write_str(EXE_SUFFIX)?;
2638 Ok(())
2639 }
2640}
2641
2642impl VersionRequest {
2643 pub fn from_specifiers(specifiers: VersionSpecifiers, variant: PythonVariant) -> Self {
2648 if let [specifier] = specifiers.iter().as_slice() {
2649 if specifier.operator() == &uv_pep440::Operator::Equal {
2650 if let Ok(request) = Self::from_str(&specifier.version().to_string()) {
2651 return request;
2652 }
2653 }
2654 }
2655 Self::Range(specifiers, variant)
2656 }
2657
2658 #[must_use]
2660 pub fn only_minor(self) -> Self {
2661 match self {
2662 Self::Any => self,
2663 Self::Default => self,
2664 Self::Range(specifiers, variant) => Self::Range(
2665 specifiers
2666 .into_iter()
2667 .map(|s| s.only_minor_release())
2668 .collect(),
2669 variant,
2670 ),
2671 Self::Major(..) => self,
2672 Self::MajorMinor(..) => self,
2673 Self::MajorMinorPatch(major, minor, _, variant)
2674 | Self::MajorMinorPrerelease(major, minor, _, variant)
2675 | Self::MajorMinorPatchPrerelease(major, minor, _, _, variant) => {
2676 Self::MajorMinor(major, minor, variant)
2677 }
2678 }
2679 }
2680
2681 pub(crate) fn executable_names(
2683 &self,
2684 implementation: Option<&ImplementationName>,
2685 ) -> Vec<ExecutableName> {
2686 let prerelease = match self {
2687 Self::MajorMinorPrerelease(_, _, prerelease, _)
2688 | Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => {
2689 Some(prerelease)
2691 }
2692 _ => None,
2693 };
2694
2695 let mut names = Vec::new();
2697 names.push(ExecutableName::default());
2698
2699 if let Some(major) = self.major() {
2701 names.push(ExecutableName::default().with_major(major));
2703 if let Some(minor) = self.minor() {
2704 names.push(
2706 ExecutableName::default()
2707 .with_major(major)
2708 .with_minor(minor),
2709 );
2710 if let Some(patch) = self.patch() {
2711 names.push(
2713 ExecutableName::default()
2714 .with_major(major)
2715 .with_minor(minor)
2716 .with_patch(patch),
2717 );
2718 }
2719 }
2720 } else {
2721 names.push(ExecutableName::default().with_major(3));
2723 }
2724
2725 if let Some(prerelease) = prerelease {
2726 for i in 0..names.len() {
2728 let name = names[i];
2729 if name.minor.is_none() {
2730 continue;
2733 }
2734 names.push(name.with_prerelease(*prerelease));
2735 }
2736 }
2737
2738 if let Some(implementation) = implementation {
2740 for i in 0..names.len() {
2741 let name = names[i].with_implementation(*implementation);
2742 names.push(name);
2743 }
2744 } else {
2745 if matches!(self, Self::Any) {
2747 for i in 0..names.len() {
2748 for implementation in ImplementationName::iter_all() {
2749 let name = names[i].with_implementation(implementation);
2750 names.push(name);
2751 }
2752 }
2753 }
2754 }
2755
2756 if let Some(variant) = self.variant() {
2758 if variant != PythonVariant::Default {
2759 for i in 0..names.len() {
2760 let name = names[i].with_variant(variant);
2761 names.push(name);
2762 }
2763 }
2764 }
2765
2766 names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
2767 names.reverse();
2768
2769 names
2770 }
2771
2772 pub(crate) fn major(&self) -> Option<u8> {
2774 match self {
2775 Self::Any | Self::Default | Self::Range(_, _) => None,
2776 Self::Major(major, _) => Some(*major),
2777 Self::MajorMinor(major, _, _) => Some(*major),
2778 Self::MajorMinorPatch(major, _, _, _) => Some(*major),
2779 Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2780 Self::MajorMinorPatchPrerelease(major, _, _, _, _) => Some(*major),
2781 }
2782 }
2783
2784 pub(crate) fn minor(&self) -> Option<u8> {
2786 match self {
2787 Self::Any | Self::Default | Self::Range(_, _) => None,
2788 Self::Major(_, _) => None,
2789 Self::MajorMinor(_, minor, _) => Some(*minor),
2790 Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
2791 Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2792 Self::MajorMinorPatchPrerelease(_, minor, _, _, _) => Some(*minor),
2793 }
2794 }
2795
2796 pub(crate) fn patch(&self) -> Option<u8> {
2798 match self {
2799 Self::Any | Self::Default | Self::Range(_, _) => None,
2800 Self::Major(_, _) => None,
2801 Self::MajorMinor(_, _, _) => None,
2802 Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
2803 Self::MajorMinorPrerelease(_, _, _, _) => None,
2804 Self::MajorMinorPatchPrerelease(_, _, patch, _, _) => Some(*patch),
2805 }
2806 }
2807
2808 pub(crate) fn prerelease(&self) -> Option<&Prerelease> {
2810 match self {
2811 Self::Any | Self::Default | Self::Range(_, _) => None,
2812 Self::Major(_, _) => None,
2813 Self::MajorMinor(_, _, _) => None,
2814 Self::MajorMinorPatch(_, _, _, _) => None,
2815 Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
2816 Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => Some(prerelease),
2817 }
2818 }
2819
2820 pub(crate) fn check_supported(&self) -> Result<(), String> {
2824 match self {
2825 Self::Any | Self::Default => (),
2826 Self::Major(major, _) => {
2827 if *major < 3 {
2828 return Err(format!(
2829 "Python <3 is not supported but {major} was requested."
2830 ));
2831 }
2832 }
2833 Self::MajorMinor(major, minor, _) => {
2834 if (*major, *minor) < (3, 6) {
2835 return Err(format!(
2836 "Python <3.6 is not supported but {major}.{minor} was requested."
2837 ));
2838 }
2839 }
2840 Self::MajorMinorPatch(major, minor, patch, _) => {
2841 if (*major, *minor) < (3, 6) {
2842 return Err(format!(
2843 "Python <3.6 is not supported but {major}.{minor}.{patch} was requested."
2844 ));
2845 }
2846 }
2847 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2848 if (*major, *minor) < (3, 6) {
2849 return Err(format!(
2850 "Python <3.6 is not supported but {major}.{minor}{prerelease} was requested."
2851 ));
2852 }
2853 }
2854 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
2855 if (*major, *minor) < (3, 6) {
2856 return Err(format!(
2857 "Python <3.6 is not supported but {major}.{minor}.{patch}{prerelease} was requested."
2858 ));
2859 }
2860 }
2861 Self::Range(_, _) => (),
2863 }
2864
2865 if self.is_freethreaded() {
2866 if let Self::MajorMinor(major, minor, _) = self.clone().without_patch() {
2867 if (major, minor) < (3, 13) {
2868 return Err(format!(
2869 "Python <3.13 does not support free-threading but {self} was requested."
2870 ));
2871 }
2872 }
2873 }
2874
2875 Ok(())
2876 }
2877
2878 #[must_use]
2884 pub(crate) fn into_request_for_source(self, source: PythonSource) -> Self {
2885 match self {
2886 Self::Default => match source {
2887 PythonSource::ParentInterpreter
2888 | PythonSource::CondaPrefix
2889 | PythonSource::BaseCondaPrefix
2890 | PythonSource::ProvidedPath
2891 | PythonSource::DiscoveredEnvironment
2892 | PythonSource::ActiveEnvironment => Self::Any,
2893 PythonSource::SearchPath
2894 | PythonSource::SearchPathFirst
2895 | PythonSource::Registry
2896 | PythonSource::MicrosoftStore
2897 | PythonSource::Managed => Self::Default,
2898 },
2899 _ => self,
2900 }
2901 }
2902
2903 pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
2905 match self {
2906 Self::Any => true,
2907 Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
2909 Self::Major(major, variant) => {
2910 interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
2911 }
2912 Self::MajorMinor(major, minor, variant) => {
2913 (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
2914 && variant.matches_interpreter(interpreter)
2915 }
2916 Self::MajorMinorPatch(major, minor, patch, variant) => {
2917 (
2918 interpreter.python_major(),
2919 interpreter.python_minor(),
2920 interpreter.python_patch(),
2921 ) == (*major, *minor, *patch)
2922 && interpreter.python_version().pre().is_none()
2925 && variant.matches_interpreter(interpreter)
2926 }
2927 Self::Range(specifiers, variant) => {
2928 let version = if specifiers
2931 .iter()
2932 .any(uv_pep440::VersionSpecifier::any_prerelease)
2933 {
2934 Cow::Borrowed(interpreter.python_version())
2935 } else {
2936 Cow::Owned(interpreter.python_version().only_release())
2937 };
2938 specifiers.contains(&version) && variant.matches_interpreter(interpreter)
2939 }
2940 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
2941 let version = interpreter.python_version();
2942 let Some(interpreter_prerelease) = version.pre() else {
2943 return false;
2944 };
2945 (
2946 interpreter.python_major(),
2947 interpreter.python_minor(),
2948 interpreter_prerelease,
2949 ) == (*major, *minor, *prerelease)
2950 && variant.matches_interpreter(interpreter)
2951 }
2952 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
2953 let version = interpreter.python_version();
2954 let Some(interpreter_prerelease) = version.pre() else {
2955 return false;
2956 };
2957 (
2958 interpreter.python_major(),
2959 interpreter.python_minor(),
2960 interpreter.python_patch(),
2961 interpreter_prerelease,
2962 ) == (*major, *minor, *patch, *prerelease)
2963 && variant.matches_interpreter(interpreter)
2964 }
2965 }
2966 }
2967
2968 pub(crate) fn matches_version(&self, version: &PythonVersion) -> bool {
2973 match self {
2974 Self::Any | Self::Default => true,
2975 Self::Major(major, _) => version.major() == *major,
2976 Self::MajorMinor(major, minor, _) => {
2977 (version.major(), version.minor()) == (*major, *minor)
2978 }
2979 Self::MajorMinorPatch(major, minor, patch, _) => {
2980 (version.major(), version.minor(), version.patch())
2981 == (*major, *minor, Some(*patch))
2982 }
2983 Self::Range(specifiers, _) => {
2984 let version = if specifiers
2987 .iter()
2988 .any(uv_pep440::VersionSpecifier::any_prerelease)
2989 {
2990 Cow::Borrowed(&version.version)
2991 } else {
2992 Cow::Owned(version.version.only_release())
2993 };
2994 specifiers.contains(&version)
2995 }
2996 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2997 (version.major(), version.minor(), version.pre())
2998 == (*major, *minor, Some(*prerelease))
2999 }
3000 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3001 (
3002 version.major(),
3003 version.minor(),
3004 version.patch(),
3005 version.pre(),
3006 ) == (*major, *minor, Some(*patch), Some(*prerelease))
3007 }
3008 }
3009 }
3010
3011 fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
3016 match self {
3017 Self::Any | Self::Default => true,
3018 Self::Major(self_major, _) => *self_major == major,
3019 Self::MajorMinor(self_major, self_minor, _) => {
3020 (*self_major, *self_minor) == (major, minor)
3021 }
3022 Self::MajorMinorPatch(self_major, self_minor, _, _) => {
3023 (*self_major, *self_minor) == (major, minor)
3024 }
3025 Self::Range(specifiers, _) => {
3026 let range = release_specifiers_to_ranges(specifiers.clone());
3027 let Some((lower, upper)) = range.bounding_range() else {
3028 return true;
3029 };
3030 let version = Version::new([u64::from(major), u64::from(minor)]);
3031
3032 let lower = LowerBound::new(lower.cloned());
3033 if !lower.major_minor().contains(&version) {
3034 return false;
3035 }
3036
3037 let upper = UpperBound::new(upper.cloned());
3038 if !upper.major_minor().contains(&version) {
3039 return false;
3040 }
3041
3042 true
3043 }
3044 Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
3045 (*self_major, *self_minor) == (major, minor)
3046 }
3047 Self::MajorMinorPatchPrerelease(self_major, self_minor, _, _, _) => {
3048 (*self_major, *self_minor) == (major, minor)
3049 }
3050 }
3051 }
3052
3053 pub(crate) fn matches_major_minor_patch_prerelease(
3059 &self,
3060 major: u8,
3061 minor: u8,
3062 patch: u8,
3063 prerelease: Option<Prerelease>,
3064 ) -> bool {
3065 match self {
3066 Self::Any | Self::Default => true,
3067 Self::Major(self_major, _) => *self_major == major,
3068 Self::MajorMinor(self_major, self_minor, _) => {
3069 (*self_major, *self_minor) == (major, minor)
3070 }
3071 Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
3072 (*self_major, *self_minor, *self_patch) == (major, minor, patch)
3073 && prerelease.is_none()
3076 }
3077 Self::Range(specifiers, _) => specifiers.contains(
3078 &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
3079 .with_pre(prerelease),
3080 ),
3081 Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3082 (*self_major, *self_minor, 0, Some(*self_prerelease))
3084 == (major, minor, patch, prerelease)
3085 }
3086 Self::MajorMinorPatchPrerelease(
3087 self_major,
3088 self_minor,
3089 self_patch,
3090 self_prerelease,
3091 _,
3092 ) => {
3093 (
3094 *self_major,
3095 *self_minor,
3096 *self_patch,
3097 Some(*self_prerelease),
3098 ) == (major, minor, patch, prerelease)
3099 }
3100 }
3101 }
3102
3103 pub(crate) fn matches_installation_key(&self, key: &PythonInstallationKey) -> bool {
3108 self.matches_major_minor_patch_prerelease(key.major, key.minor, key.patch, key.prerelease())
3109 }
3110
3111 fn has_patch(&self) -> bool {
3113 match self {
3114 Self::Any | Self::Default => false,
3115 Self::Major(..) => false,
3116 Self::MajorMinor(..) => false,
3117 Self::MajorMinorPatch(..) => true,
3118 Self::MajorMinorPrerelease(..) => false,
3119 Self::MajorMinorPatchPrerelease(..) => true,
3120 Self::Range(_, _) => false,
3121 }
3122 }
3123
3124 #[must_use]
3128 fn without_patch(self) -> Self {
3129 match self {
3130 Self::Default => Self::Default,
3131 Self::Any => Self::Any,
3132 Self::Major(major, variant) => Self::Major(major, variant),
3133 Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3134 Self::MajorMinorPatch(major, minor, _, variant) => {
3135 Self::MajorMinor(major, minor, variant)
3136 }
3137 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3138 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3139 }
3140 Self::MajorMinorPatchPrerelease(major, minor, _, prerelease, variant) => {
3141 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3142 }
3143 Self::Range(_, _) => self,
3144 }
3145 }
3146
3147 pub(crate) fn allows_prereleases(&self) -> bool {
3149 match self {
3150 Self::Default => false,
3151 Self::Any => true,
3152 Self::Major(..) => false,
3153 Self::MajorMinor(..) => false,
3154 Self::MajorMinorPatch(..) => false,
3155 Self::MajorMinorPrerelease(..) => true,
3156 Self::MajorMinorPatchPrerelease(..) => true,
3157 Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3158 }
3159 }
3160
3161 pub(crate) fn is_debug(&self) -> bool {
3163 match self {
3164 Self::Any | Self::Default => false,
3165 Self::Major(_, variant)
3166 | Self::MajorMinor(_, _, variant)
3167 | Self::MajorMinorPatch(_, _, _, variant)
3168 | Self::MajorMinorPrerelease(_, _, _, variant)
3169 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3170 | Self::Range(_, variant) => variant.is_debug(),
3171 }
3172 }
3173
3174 pub(crate) fn is_freethreaded(&self) -> bool {
3176 match self {
3177 Self::Any | Self::Default => false,
3178 Self::Major(_, variant)
3179 | Self::MajorMinor(_, _, variant)
3180 | Self::MajorMinorPatch(_, _, _, variant)
3181 | Self::MajorMinorPrerelease(_, _, _, variant)
3182 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3183 | Self::Range(_, variant) => variant.is_freethreaded(),
3184 }
3185 }
3186
3187 #[must_use]
3191 pub fn without_python_variant(self) -> Self {
3192 match self {
3195 Self::Any | Self::Default => self,
3196 Self::Major(major, _) => Self::Major(major, PythonVariant::Default),
3197 Self::MajorMinor(major, minor, _) => {
3198 Self::MajorMinor(major, minor, PythonVariant::Default)
3199 }
3200 Self::MajorMinorPatch(major, minor, patch, _) => {
3201 Self::MajorMinorPatch(major, minor, patch, PythonVariant::Default)
3202 }
3203 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3204 Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Default)
3205 }
3206 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3207 Self::MajorMinorPatchPrerelease(
3208 major,
3209 minor,
3210 patch,
3211 prerelease,
3212 PythonVariant::Default,
3213 )
3214 }
3215 Self::Range(specifiers, _) => Self::Range(specifiers, PythonVariant::Default),
3216 }
3217 }
3218
3219 pub(crate) fn variant(&self) -> Option<PythonVariant> {
3221 match self {
3222 Self::Any => None,
3223 Self::Default => Some(PythonVariant::Default),
3224 Self::Major(_, variant)
3225 | Self::MajorMinor(_, _, variant)
3226 | Self::MajorMinorPatch(_, _, _, variant)
3227 | Self::MajorMinorPrerelease(_, _, _, variant)
3228 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3229 | Self::Range(_, variant) => Some(*variant),
3230 }
3231 }
3232
3233 pub fn as_pep440_version(&self) -> Option<Version> {
3237 match self {
3238 Self::Default | Self::Any | Self::Range(_, _) => None,
3239 Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3240 Self::MajorMinor(major, minor, _) => {
3241 Some(Version::new([u64::from(*major), u64::from(*minor)]))
3242 }
3243 Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3244 u64::from(*major),
3245 u64::from(*minor),
3246 u64::from(*patch),
3247 ])),
3248 Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3250 Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3251 ),
3252 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => Some(
3253 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3254 .with_pre(Some(*prerelease)),
3255 ),
3256 }
3257 }
3258
3259 pub fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
3265 match self {
3266 Self::Default | Self::Any => None,
3267 Self::Major(major, _) => Some(VersionSpecifiers::from(
3268 VersionSpecifier::equals_star_version(Version::new([u64::from(*major)])),
3269 )),
3270 Self::MajorMinor(major, minor, _) => Some(VersionSpecifiers::from(
3271 VersionSpecifier::equals_star_version(Version::new([
3272 u64::from(*major),
3273 u64::from(*minor),
3274 ])),
3275 )),
3276 Self::MajorMinorPatch(major, minor, patch, _) => {
3277 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3278 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)]),
3279 )))
3280 }
3281 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3282 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3283 Version::new([u64::from(*major), u64::from(*minor), 0])
3284 .with_pre(Some(*prerelease)),
3285 )))
3286 }
3287 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3288 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3289 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3290 .with_pre(Some(*prerelease)),
3291 )))
3292 }
3293 Self::Range(specifiers, _) => Some(specifiers.clone()),
3294 }
3295 }
3296}
3297
3298impl FromStr for VersionRequest {
3299 type Err = Error;
3300
3301 fn from_str(s: &str) -> Result<Self, Self::Err> {
3302 fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3305 if s.chars().all(char::is_alphabetic) {
3307 return Err(Error::InvalidVersionRequest(s.to_string()));
3308 }
3309
3310 let Some(mut start) = s.rfind(|c: char| c.is_numeric()) else {
3311 return Ok((s, PythonVariant::Default));
3312 };
3313
3314 start += 1;
3316
3317 if start + 1 > s.len() {
3319 return Ok((s, PythonVariant::Default));
3320 }
3321
3322 let variant = &s[start..];
3323 let prefix = &s[..start];
3324
3325 let variant = variant.strip_prefix('+').unwrap_or(variant);
3327
3328 let Ok(variant) = PythonVariant::from_str(variant) else {
3332 return Ok((s, PythonVariant::Default));
3333 };
3334
3335 Ok((prefix, variant))
3336 }
3337
3338 let (s, variant) = parse_variant(s)?;
3339 let Ok(version) = Version::from_str(s) else {
3340 return parse_version_specifiers_request(s, variant);
3341 };
3342
3343 let version = split_wheel_tag_release_version(version);
3345
3346 if version.post().is_some() || version.dev().is_some() {
3348 return Err(Error::InvalidVersionRequest(s.to_string()));
3349 }
3350
3351 if !version.local().is_empty() {
3354 return Err(Error::InvalidVersionRequest(s.to_string()));
3355 }
3356
3357 let Ok(release) = try_into_u8_slice(&version.release()) else {
3359 return Err(Error::InvalidVersionRequest(s.to_string()));
3360 };
3361
3362 let prerelease = version.pre();
3363
3364 match release.as_slice() {
3365 [major] => {
3367 if prerelease.is_some() {
3369 return Err(Error::InvalidVersionRequest(s.to_string()));
3370 }
3371 Ok(Self::Major(*major, variant))
3372 }
3373 [major, minor] => {
3375 if let Some(prerelease) = prerelease {
3376 return Ok(Self::MajorMinorPrerelease(
3377 *major, *minor, prerelease, variant,
3378 ));
3379 }
3380 Ok(Self::MajorMinor(*major, *minor, variant))
3381 }
3382 [major, minor, patch] => {
3384 if let Some(prerelease) = prerelease {
3385 if *patch == 0 {
3386 return Ok(Self::MajorMinorPrerelease(
3387 *major, *minor, prerelease, variant,
3388 ));
3389 }
3390 return Ok(Self::MajorMinorPatchPrerelease(
3391 *major, *minor, *patch, prerelease, variant,
3392 ));
3393 }
3394 Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3395 }
3396 _ => Err(Error::InvalidVersionRequest(s.to_string())),
3397 }
3398 }
3399}
3400
3401impl FromStr for PythonVariant {
3402 type Err = ();
3403
3404 fn from_str(s: &str) -> Result<Self, Self::Err> {
3405 match s {
3406 "t" | "freethreaded" => Ok(Self::Freethreaded),
3407 "d" | "debug" => Ok(Self::Debug),
3408 "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3409 "gil" => Ok(Self::Gil),
3410 "gil+debug" => Ok(Self::GilDebug),
3411 "" => Ok(Self::Default),
3412 _ => Err(()),
3413 }
3414 }
3415}
3416
3417impl fmt::Display for PythonVariant {
3418 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3419 match self {
3420 Self::Default => f.write_str("default"),
3421 Self::Debug => f.write_str("debug"),
3422 Self::Freethreaded => f.write_str("freethreaded"),
3423 Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3424 Self::Gil => f.write_str("gil"),
3425 Self::GilDebug => f.write_str("gil+debug"),
3426 }
3427 }
3428}
3429
3430fn parse_version_specifiers_request(
3431 s: &str,
3432 variant: PythonVariant,
3433) -> Result<VersionRequest, Error> {
3434 let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3435 return Err(Error::InvalidVersionRequest(s.to_string()));
3436 };
3437 if specifiers.is_empty() {
3438 return Err(Error::InvalidVersionRequest(s.to_string()));
3439 }
3440 Ok(VersionRequest::from_specifiers(specifiers, variant))
3441}
3442
3443impl From<&PythonVersion> for VersionRequest {
3444 fn from(version: &PythonVersion) -> Self {
3445 Self::from_str(&version.string)
3446 .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3447 }
3448}
3449
3450impl fmt::Display for VersionRequest {
3451 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3452 match self {
3453 Self::Any => f.write_str("any"),
3454 Self::Default => f.write_str("default"),
3455 Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3456 Self::MajorMinor(major, minor, variant) => {
3457 write!(f, "{major}.{minor}{}", variant.display_suffix())
3458 }
3459 Self::MajorMinorPatch(major, minor, patch, variant) => {
3460 write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3461 }
3462 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3463 write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3464 }
3465 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3466 write!(
3467 f,
3468 "{major}.{minor}.{patch}{prerelease}{}",
3469 variant.display_suffix()
3470 )
3471 }
3472 Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3473 }
3474 }
3475}
3476
3477impl fmt::Display for PythonRequest {
3478 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3479 match self {
3480 Self::Default => write!(f, "a default Python"),
3481 Self::Any => write!(f, "any Python"),
3482 Self::Version(version) => write!(f, "Python {version}"),
3483 Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3484 Self::File(path) => write!(f, "path `{}`", path.user_display()),
3485 Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3486 Self::Implementation(implementation) => {
3487 write!(f, "{}", implementation.pretty())
3488 }
3489 Self::ImplementationVersion(implementation, version) => {
3490 write!(f, "{} {version}", implementation.pretty())
3491 }
3492 Self::Key(request) => write!(f, "{request}"),
3493 }
3494 }
3495}
3496
3497impl fmt::Display for PythonSource {
3498 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3499 match self {
3500 Self::ProvidedPath => f.write_str("provided path"),
3501 Self::ActiveEnvironment => f.write_str("active virtual environment"),
3502 Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3503 Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3504 Self::SearchPath => f.write_str("search path"),
3505 Self::SearchPathFirst => f.write_str("first executable in the search path"),
3506 Self::Registry => f.write_str("registry"),
3507 Self::MicrosoftStore => f.write_str("Microsoft Store"),
3508 Self::Managed => f.write_str("managed installations"),
3509 Self::ParentInterpreter => f.write_str("parent interpreter"),
3510 }
3511 }
3512}
3513
3514impl PythonPreference {
3515 fn sources(self) -> &'static [PythonSource] {
3518 match self {
3519 Self::OnlyManaged => &[PythonSource::Managed],
3520 Self::Managed => {
3521 if cfg!(windows) {
3522 &[
3523 PythonSource::Managed,
3524 PythonSource::SearchPath,
3525 PythonSource::Registry,
3526 ]
3527 } else {
3528 &[PythonSource::Managed, PythonSource::SearchPath]
3529 }
3530 }
3531 Self::System => {
3532 if cfg!(windows) {
3533 &[
3534 PythonSource::SearchPath,
3535 PythonSource::Registry,
3536 PythonSource::Managed,
3537 ]
3538 } else {
3539 &[PythonSource::SearchPath, PythonSource::Managed]
3540 }
3541 }
3542 Self::OnlySystem => {
3543 if cfg!(windows) {
3544 &[PythonSource::SearchPath, PythonSource::Registry]
3545 } else {
3546 &[PythonSource::SearchPath]
3547 }
3548 }
3549 }
3550 }
3551
3552 pub fn canonical_name(&self) -> &'static str {
3556 match self {
3557 Self::OnlyManaged => "only managed",
3558 Self::Managed => "prefer managed",
3559 Self::System => "prefer system",
3560 Self::OnlySystem => "only system",
3561 }
3562 }
3563}
3564
3565impl fmt::Display for PythonPreference {
3566 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3567 f.write_str(match self {
3568 Self::OnlyManaged => "only managed",
3569 Self::Managed => "prefer managed",
3570 Self::System => "prefer system",
3571 Self::OnlySystem => "only system",
3572 })
3573 }
3574}
3575
3576impl DiscoveryPreferences {
3577 fn sources(&self, request: &PythonRequest) -> String {
3580 let python_sources = self
3581 .python_preference
3582 .sources()
3583 .iter()
3584 .map(ToString::to_string)
3585 .collect::<Vec<_>>();
3586 match self.environment_preference {
3587 EnvironmentPreference::Any => disjunction(
3588 &["virtual environments"]
3589 .into_iter()
3590 .chain(python_sources.iter().map(String::as_str))
3591 .collect::<Vec<_>>(),
3592 ),
3593 EnvironmentPreference::ExplicitSystem => {
3594 if request.is_explicit_system() {
3595 disjunction(
3596 &["virtual environments"]
3597 .into_iter()
3598 .chain(python_sources.iter().map(String::as_str))
3599 .collect::<Vec<_>>(),
3600 )
3601 } else {
3602 disjunction(&["virtual environments"])
3603 }
3604 }
3605 EnvironmentPreference::OnlySystem => disjunction(
3606 &python_sources
3607 .iter()
3608 .map(String::as_str)
3609 .collect::<Vec<_>>(),
3610 ),
3611 EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3612 }
3613 }
3614}
3615
3616impl fmt::Display for PythonNotFound {
3617 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3618 let sources = DiscoveryPreferences {
3619 python_preference: self.python_preference,
3620 environment_preference: self.environment_preference,
3621 }
3622 .sources(&self.request);
3623
3624 match self.request {
3625 PythonRequest::Default | PythonRequest::Any => {
3626 write!(f, "No interpreter found in {sources}")
3627 }
3628 PythonRequest::File(_) => {
3629 write!(f, "No interpreter found at {}", self.request)
3630 }
3631 PythonRequest::Directory(_) => {
3632 write!(f, "No interpreter found in {}", self.request)
3633 }
3634 _ => {
3635 write!(f, "No interpreter found for {} in {sources}", self.request)
3636 }
3637 }
3638 }
3639}
3640
3641fn disjunction(items: &[&str]) -> String {
3643 match items.len() {
3644 0 => String::new(),
3645 1 => items[0].to_string(),
3646 2 => format!("{} or {}", items[0], items[1]),
3647 _ => {
3648 let last = items.last().unwrap();
3649 format!(
3650 "{}, or {}",
3651 items.iter().take(items.len() - 1).join(", "),
3652 last
3653 )
3654 }
3655 }
3656}
3657
3658fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3659 release
3660 .iter()
3661 .map(|x| match u8::try_from(*x) {
3662 Ok(x) => Ok(x),
3663 Err(e) => Err(e),
3664 })
3665 .collect()
3666}
3667
3668fn split_wheel_tag_release_version(version: Version) -> Version {
3675 let release = version.release();
3676 if release.len() != 1 {
3677 return version;
3678 }
3679
3680 let release = release[0].to_string();
3681 let mut chars = release.chars();
3682 let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3683 return version;
3684 };
3685
3686 let Ok(minor) = chars.as_str().parse::<u32>() else {
3687 return version;
3688 };
3689
3690 version.with_release([u64::from(major), u64::from(minor)])
3691}
3692
3693#[cfg(test)]
3694mod tests {
3695 use std::{path::PathBuf, str::FromStr};
3696
3697 use assert_fs::{TempDir, prelude::*};
3698 use target_lexicon::{Aarch64Architecture, Architecture};
3699 use test_log::test;
3700 use uv_distribution_types::RequiresPython;
3701 use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3702
3703 use crate::{
3704 discovery::{PythonRequest, VersionRequest},
3705 downloads::{ArchRequest, PythonDownloadRequest},
3706 implementation::ImplementationName,
3707 };
3708 use uv_platform::{Arch, Libc, Os};
3709
3710 use super::{
3711 DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonVariant,
3712 };
3713
3714 #[test]
3715 fn interpreter_request_from_str() {
3716 assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3717 assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
3718 assert_eq!(
3719 PythonRequest::parse("3.12"),
3720 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
3721 );
3722 assert_eq!(
3723 PythonRequest::parse(">=3.12"),
3724 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3725 );
3726 assert_eq!(
3727 PythonRequest::parse(">=3.12,<3.13"),
3728 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3729 );
3730 assert_eq!(
3731 PythonRequest::parse(">=3.12,<3.13"),
3732 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3733 );
3734
3735 assert_eq!(
3736 PythonRequest::parse("3.13.0a1"),
3737 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3738 );
3739 assert_eq!(
3740 PythonRequest::parse("3.13.0b5"),
3741 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3742 );
3743 assert_eq!(
3744 PythonRequest::parse("3.13.0rc1"),
3745 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3746 );
3747 assert_eq!(
3748 PythonRequest::parse("3.13.1rc1"),
3749 PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3750 "Pre-release version requests require a patch version of zero"
3751 );
3752 assert_eq!(
3753 PythonRequest::parse("3rc1"),
3754 PythonRequest::ExecutableName("3rc1".to_string()),
3755 "Pre-release version requests require a minor version"
3756 );
3757
3758 assert_eq!(
3759 PythonRequest::parse("cpython"),
3760 PythonRequest::Implementation(ImplementationName::CPython)
3761 );
3762
3763 assert_eq!(
3764 PythonRequest::parse("cpython3.12.2"),
3765 PythonRequest::ImplementationVersion(
3766 ImplementationName::CPython,
3767 VersionRequest::from_str("3.12.2").unwrap(),
3768 )
3769 );
3770
3771 assert_eq!(
3772 PythonRequest::parse("cpython-3.13.2"),
3773 PythonRequest::Key(PythonDownloadRequest {
3774 version: Some(VersionRequest::MajorMinorPatch(
3775 3,
3776 13,
3777 2,
3778 PythonVariant::Default
3779 )),
3780 implementation: Some(ImplementationName::CPython),
3781 arch: None,
3782 os: None,
3783 libc: None,
3784 build: None,
3785 prereleases: None
3786 })
3787 );
3788 assert_eq!(
3789 PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3790 PythonRequest::Key(PythonDownloadRequest {
3791 version: Some(VersionRequest::MajorMinorPatch(
3792 3,
3793 13,
3794 2,
3795 PythonVariant::Default
3796 )),
3797 implementation: Some(ImplementationName::CPython),
3798 arch: Some(ArchRequest::Explicit(Arch::new(
3799 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3800 None
3801 ))),
3802 os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3803 libc: Some(Libc::None),
3804 build: None,
3805 prereleases: None
3806 })
3807 );
3808 assert_eq!(
3809 PythonRequest::parse("any-3.13.2"),
3810 PythonRequest::Key(PythonDownloadRequest {
3811 version: Some(VersionRequest::MajorMinorPatch(
3812 3,
3813 13,
3814 2,
3815 PythonVariant::Default
3816 )),
3817 implementation: None,
3818 arch: None,
3819 os: None,
3820 libc: None,
3821 build: None,
3822 prereleases: None
3823 })
3824 );
3825 assert_eq!(
3826 PythonRequest::parse("any-3.13.2-any-aarch64"),
3827 PythonRequest::Key(PythonDownloadRequest {
3828 version: Some(VersionRequest::MajorMinorPatch(
3829 3,
3830 13,
3831 2,
3832 PythonVariant::Default
3833 )),
3834 implementation: None,
3835 arch: Some(ArchRequest::Explicit(Arch::new(
3836 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3837 None
3838 ))),
3839 os: None,
3840 libc: None,
3841 build: None,
3842 prereleases: None
3843 })
3844 );
3845
3846 assert_eq!(
3847 PythonRequest::parse("pypy"),
3848 PythonRequest::Implementation(ImplementationName::PyPy)
3849 );
3850 assert_eq!(
3851 PythonRequest::parse("pp"),
3852 PythonRequest::Implementation(ImplementationName::PyPy)
3853 );
3854 assert_eq!(
3855 PythonRequest::parse("graalpy"),
3856 PythonRequest::Implementation(ImplementationName::GraalPy)
3857 );
3858 assert_eq!(
3859 PythonRequest::parse("gp"),
3860 PythonRequest::Implementation(ImplementationName::GraalPy)
3861 );
3862 assert_eq!(
3863 PythonRequest::parse("cp"),
3864 PythonRequest::Implementation(ImplementationName::CPython)
3865 );
3866 assert_eq!(
3867 PythonRequest::parse("pypy3.10"),
3868 PythonRequest::ImplementationVersion(
3869 ImplementationName::PyPy,
3870 VersionRequest::from_str("3.10").unwrap(),
3871 )
3872 );
3873 assert_eq!(
3874 PythonRequest::parse("pp310"),
3875 PythonRequest::ImplementationVersion(
3876 ImplementationName::PyPy,
3877 VersionRequest::from_str("3.10").unwrap(),
3878 )
3879 );
3880 assert_eq!(
3881 PythonRequest::parse("graalpy3.10"),
3882 PythonRequest::ImplementationVersion(
3883 ImplementationName::GraalPy,
3884 VersionRequest::from_str("3.10").unwrap(),
3885 )
3886 );
3887 assert_eq!(
3888 PythonRequest::parse("gp310"),
3889 PythonRequest::ImplementationVersion(
3890 ImplementationName::GraalPy,
3891 VersionRequest::from_str("3.10").unwrap(),
3892 )
3893 );
3894 assert_eq!(
3895 PythonRequest::parse("cp38"),
3896 PythonRequest::ImplementationVersion(
3897 ImplementationName::CPython,
3898 VersionRequest::from_str("3.8").unwrap(),
3899 )
3900 );
3901 assert_eq!(
3902 PythonRequest::parse("pypy@3.10"),
3903 PythonRequest::ImplementationVersion(
3904 ImplementationName::PyPy,
3905 VersionRequest::from_str("3.10").unwrap(),
3906 )
3907 );
3908 assert_eq!(
3909 PythonRequest::parse("pypy310"),
3910 PythonRequest::ImplementationVersion(
3911 ImplementationName::PyPy,
3912 VersionRequest::from_str("3.10").unwrap(),
3913 )
3914 );
3915 assert_eq!(
3916 PythonRequest::parse("graalpy@3.10"),
3917 PythonRequest::ImplementationVersion(
3918 ImplementationName::GraalPy,
3919 VersionRequest::from_str("3.10").unwrap(),
3920 )
3921 );
3922 assert_eq!(
3923 PythonRequest::parse("graalpy310"),
3924 PythonRequest::ImplementationVersion(
3925 ImplementationName::GraalPy,
3926 VersionRequest::from_str("3.10").unwrap(),
3927 )
3928 );
3929
3930 let tempdir = TempDir::new().unwrap();
3931 assert_eq!(
3932 PythonRequest::parse(tempdir.path().to_str().unwrap()),
3933 PythonRequest::Directory(tempdir.path().to_path_buf()),
3934 "An existing directory is treated as a directory"
3935 );
3936 assert_eq!(
3937 PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3938 PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3939 "A path that does not exist is treated as a file"
3940 );
3941 tempdir.child("bar").touch().unwrap();
3942 assert_eq!(
3943 PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3944 PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3945 "An existing file is treated as a file"
3946 );
3947 assert_eq!(
3948 PythonRequest::parse("./foo"),
3949 PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
3950 "A string with a file system separator is treated as a file"
3951 );
3952 assert_eq!(
3953 PythonRequest::parse("3.13t"),
3954 PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
3955 );
3956 }
3957
3958 #[test]
3959 fn discovery_sources_prefer_system_orders_search_path_first() {
3960 let preferences = DiscoveryPreferences {
3961 python_preference: PythonPreference::System,
3962 environment_preference: EnvironmentPreference::OnlySystem,
3963 };
3964 let sources = preferences.sources(&PythonRequest::Default);
3965
3966 if cfg!(windows) {
3967 assert_eq!(sources, "search path, registry, or managed installations");
3968 } else {
3969 assert_eq!(sources, "search path or managed installations");
3970 }
3971 }
3972
3973 #[test]
3974 fn discovery_sources_only_system_matches_platform_order() {
3975 let preferences = DiscoveryPreferences {
3976 python_preference: PythonPreference::OnlySystem,
3977 environment_preference: EnvironmentPreference::OnlySystem,
3978 };
3979 let sources = preferences.sources(&PythonRequest::Default);
3980
3981 if cfg!(windows) {
3982 assert_eq!(sources, "search path or registry");
3983 } else {
3984 assert_eq!(sources, "search path");
3985 }
3986 }
3987
3988 #[test]
3989 fn interpreter_request_to_canonical_string() {
3990 assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
3991 assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
3992 assert_eq!(
3993 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
3994 "3.12"
3995 );
3996 assert_eq!(
3997 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3998 .to_canonical_string(),
3999 ">=3.12"
4000 );
4001 assert_eq!(
4002 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
4003 .to_canonical_string(),
4004 ">=3.12, <3.13"
4005 );
4006
4007 assert_eq!(
4008 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
4009 .to_canonical_string(),
4010 "3.13a1"
4011 );
4012
4013 assert_eq!(
4014 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
4015 .to_canonical_string(),
4016 "3.13b5"
4017 );
4018
4019 assert_eq!(
4020 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
4021 .to_canonical_string(),
4022 "3.13rc1"
4023 );
4024
4025 assert_eq!(
4026 PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
4027 .to_canonical_string(),
4028 "3.13rc4"
4029 );
4030
4031 assert_eq!(
4032 PythonRequest::Version(VersionRequest::from_str("3.14.5rc1").unwrap())
4033 .to_canonical_string(),
4034 "3.14.5rc1"
4035 );
4036
4037 assert_eq!(
4038 PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
4039 "foo"
4040 );
4041 assert_eq!(
4042 PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
4043 "cpython"
4044 );
4045 assert_eq!(
4046 PythonRequest::ImplementationVersion(
4047 ImplementationName::CPython,
4048 VersionRequest::from_str("3.12.2").unwrap(),
4049 )
4050 .to_canonical_string(),
4051 "cpython@3.12.2"
4052 );
4053 assert_eq!(
4054 PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
4055 "pypy"
4056 );
4057 assert_eq!(
4058 PythonRequest::ImplementationVersion(
4059 ImplementationName::PyPy,
4060 VersionRequest::from_str("3.10").unwrap(),
4061 )
4062 .to_canonical_string(),
4063 "pypy@3.10"
4064 );
4065 assert_eq!(
4066 PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
4067 "graalpy"
4068 );
4069 assert_eq!(
4070 PythonRequest::ImplementationVersion(
4071 ImplementationName::GraalPy,
4072 VersionRequest::from_str("3.10").unwrap(),
4073 )
4074 .to_canonical_string(),
4075 "graalpy@3.10"
4076 );
4077
4078 let tempdir = TempDir::new().unwrap();
4079 assert_eq!(
4080 PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
4081 tempdir.path().to_str().unwrap(),
4082 "An existing directory is treated as a directory"
4083 );
4084 assert_eq!(
4085 PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
4086 tempdir.child("foo").path().to_str().unwrap(),
4087 "A path that does not exist is treated as a file"
4088 );
4089 tempdir.child("bar").touch().unwrap();
4090 assert_eq!(
4091 PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
4092 tempdir.child("bar").path().to_str().unwrap(),
4093 "An existing file is treated as a file"
4094 );
4095 assert_eq!(
4096 PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
4097 "./foo",
4098 "A string with a file system separator is treated as a file"
4099 );
4100 }
4101
4102 #[test]
4103 fn version_request_from_str() {
4104 assert_eq!(
4105 VersionRequest::from_str("3").unwrap(),
4106 VersionRequest::Major(3, PythonVariant::Default)
4107 );
4108 assert_eq!(
4109 VersionRequest::from_str("3.12").unwrap(),
4110 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4111 );
4112 assert_eq!(
4113 VersionRequest::from_str("3.12.1").unwrap(),
4114 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4115 );
4116 assert!(VersionRequest::from_str("1.foo.1").is_err());
4117 assert_eq!(
4118 VersionRequest::from_str("3").unwrap(),
4119 VersionRequest::Major(3, PythonVariant::Default)
4120 );
4121 assert_eq!(
4122 VersionRequest::from_str("38").unwrap(),
4123 VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
4124 );
4125 assert_eq!(
4126 VersionRequest::from_str("312").unwrap(),
4127 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4128 );
4129 assert_eq!(
4130 VersionRequest::from_str("3100").unwrap(),
4131 VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
4132 );
4133 assert_eq!(
4134 VersionRequest::from_str("3.13a1").unwrap(),
4135 VersionRequest::MajorMinorPrerelease(
4136 3,
4137 13,
4138 Prerelease {
4139 kind: PrereleaseKind::Alpha,
4140 number: 1
4141 },
4142 PythonVariant::Default
4143 )
4144 );
4145 assert_eq!(
4146 VersionRequest::from_str("313b1").unwrap(),
4147 VersionRequest::MajorMinorPrerelease(
4148 3,
4149 13,
4150 Prerelease {
4151 kind: PrereleaseKind::Beta,
4152 number: 1
4153 },
4154 PythonVariant::Default
4155 )
4156 );
4157 assert_eq!(
4158 VersionRequest::from_str("3.13.0b2").unwrap(),
4159 VersionRequest::MajorMinorPrerelease(
4160 3,
4161 13,
4162 Prerelease {
4163 kind: PrereleaseKind::Beta,
4164 number: 2
4165 },
4166 PythonVariant::Default
4167 )
4168 );
4169 assert_eq!(
4170 VersionRequest::from_str("3.13.0rc3").unwrap(),
4171 VersionRequest::MajorMinorPrerelease(
4172 3,
4173 13,
4174 Prerelease {
4175 kind: PrereleaseKind::Rc,
4176 number: 3
4177 },
4178 PythonVariant::Default
4179 )
4180 );
4181 assert!(
4182 matches!(
4183 VersionRequest::from_str("3rc1"),
4184 Err(Error::InvalidVersionRequest(_))
4185 ),
4186 "Pre-release version requests require a minor version"
4187 );
4188 assert_eq!(
4189 VersionRequest::from_str("3.14.5rc1").unwrap(),
4190 VersionRequest::MajorMinorPatchPrerelease(
4191 3,
4192 14,
4193 5,
4194 Prerelease {
4195 kind: PrereleaseKind::Rc,
4196 number: 1
4197 },
4198 PythonVariant::Default
4199 ),
4200 "Pre-release version requests with a non-zero patch are allowed (e.g., `3.14.5rc1`)"
4201 );
4202 assert_eq!(
4203 VersionRequest::from_str("3.13.2rc1").unwrap(),
4204 VersionRequest::MajorMinorPatchPrerelease(
4205 3,
4206 13,
4207 2,
4208 Prerelease {
4209 kind: PrereleaseKind::Rc,
4210 number: 1
4211 },
4212 PythonVariant::Default
4213 )
4214 );
4215 assert!(
4216 matches!(
4217 VersionRequest::from_str("3.12-dev"),
4218 Err(Error::InvalidVersionRequest(_))
4219 ),
4220 "Development version segments are not allowed"
4221 );
4222 assert!(
4223 matches!(
4224 VersionRequest::from_str("3.12+local"),
4225 Err(Error::InvalidVersionRequest(_))
4226 ),
4227 "Local version segments are not allowed"
4228 );
4229 assert!(
4230 matches!(
4231 VersionRequest::from_str("3.12.post0"),
4232 Err(Error::InvalidVersionRequest(_))
4233 ),
4234 "Post version segments are not allowed"
4235 );
4236 assert!(
4237 matches!(
4239 VersionRequest::from_str("31000"),
4240 Err(Error::InvalidVersionRequest(_))
4241 )
4242 );
4243 assert_eq!(
4244 VersionRequest::from_str("3t").unwrap(),
4245 VersionRequest::Major(3, PythonVariant::Freethreaded)
4246 );
4247 assert_eq!(
4248 VersionRequest::from_str("313t").unwrap(),
4249 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4250 );
4251 assert_eq!(
4252 VersionRequest::from_str("3.13t").unwrap(),
4253 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4254 );
4255 assert_eq!(
4256 VersionRequest::from_str(">=3.13t").unwrap(),
4257 VersionRequest::Range(
4258 VersionSpecifiers::from_str(">=3.13").unwrap(),
4259 PythonVariant::Freethreaded
4260 )
4261 );
4262 assert_eq!(
4263 VersionRequest::from_str(">=3.13").unwrap(),
4264 VersionRequest::Range(
4265 VersionSpecifiers::from_str(">=3.13").unwrap(),
4266 PythonVariant::Default
4267 )
4268 );
4269 assert_eq!(
4270 VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4271 VersionRequest::Range(
4272 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4273 PythonVariant::Freethreaded
4274 )
4275 );
4276 assert!(matches!(
4277 VersionRequest::from_str("3.13tt"),
4278 Err(Error::InvalidVersionRequest(_))
4279 ));
4280
4281 assert_eq!(
4283 VersionRequest::from_str("==3.12").unwrap(),
4284 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4285 );
4286 assert_eq!(
4287 VersionRequest::from_str("==3.12.1").unwrap(),
4288 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4289 );
4290 }
4291
4292 #[test]
4293 fn version_request_from_specifiers() {
4294 assert_eq!(
4296 VersionRequest::from_specifiers(
4297 VersionSpecifiers::from_str("==3.12").unwrap(),
4298 PythonVariant::Default
4299 ),
4300 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4301 );
4302 assert_eq!(
4303 VersionRequest::from_specifiers(
4304 VersionSpecifiers::from_str("==3.12.1").unwrap(),
4305 PythonVariant::Default
4306 ),
4307 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4308 );
4309
4310 assert_eq!(
4312 VersionRequest::from_specifiers(
4313 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4314 PythonVariant::Default
4315 ),
4316 VersionRequest::Range(
4317 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4318 PythonVariant::Default
4319 )
4320 );
4321
4322 assert_eq!(
4324 VersionRequest::from_specifiers(
4325 VersionSpecifiers::from_str(">=3.12").unwrap(),
4326 PythonVariant::Default
4327 ),
4328 VersionRequest::Range(
4329 VersionSpecifiers::from_str(">=3.12").unwrap(),
4330 PythonVariant::Default
4331 )
4332 );
4333
4334 assert_eq!(
4336 VersionRequest::from_specifiers(
4337 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4338 PythonVariant::Default
4339 ),
4340 VersionRequest::Range(
4341 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4342 PythonVariant::Default
4343 )
4344 );
4345 }
4346
4347 #[test]
4348 fn executable_names_from_request() {
4349 fn case(request: &str, expected: &[&str]) {
4350 let (implementation, version) = match PythonRequest::parse(request) {
4351 PythonRequest::Any => (None, VersionRequest::Any),
4352 PythonRequest::Default => (None, VersionRequest::Default),
4353 PythonRequest::Version(version) => (None, version),
4354 PythonRequest::ImplementationVersion(implementation, version) => {
4355 (Some(implementation), version)
4356 }
4357 PythonRequest::Implementation(implementation) => {
4358 (Some(implementation), VersionRequest::Default)
4359 }
4360 result => {
4361 panic!("Test cases should request versions or implementations; got {result:?}")
4362 }
4363 };
4364
4365 let result: Vec<_> = version
4366 .executable_names(implementation.as_ref())
4367 .into_iter()
4368 .map(|name| name.to_string())
4369 .collect();
4370
4371 let expected: Vec<_> = expected
4372 .iter()
4373 .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4374 .collect();
4375
4376 assert_eq!(result, expected, "mismatch for case \"{request}\"");
4377 }
4378
4379 case(
4380 "any",
4381 &[
4382 "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4383 "pyodide", "pyodide3",
4384 ],
4385 );
4386
4387 case("default", &["python", "python3"]);
4388
4389 case("3", &["python3", "python"]);
4390
4391 case("4", &["python4", "python"]);
4392
4393 case("3.13", &["python3.13", "python3", "python"]);
4394
4395 case("pypy", &["pypy", "pypy3", "python", "python3"]);
4396
4397 case(
4398 "pypy@3.10",
4399 &[
4400 "pypy3.10",
4401 "pypy3",
4402 "pypy",
4403 "python3.10",
4404 "python3",
4405 "python",
4406 ],
4407 );
4408
4409 case(
4410 "3.13t",
4411 &[
4412 "python3.13t",
4413 "python3.13",
4414 "python3t",
4415 "python3",
4416 "pythont",
4417 "python",
4418 ],
4419 );
4420 case("3t", &["python3t", "python3", "pythont", "python"]);
4421
4422 case(
4423 "3.13.2",
4424 &["python3.13.2", "python3.13", "python3", "python"],
4425 );
4426
4427 case(
4428 "3.13rc2",
4429 &["python3.13rc2", "python3.13", "python3", "python"],
4430 );
4431 }
4432
4433 #[test]
4434 fn test_try_split_prefix_and_version() {
4435 assert!(matches!(
4436 PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4437 Ok(None),
4438 ));
4439 assert!(matches!(
4440 PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4441 Ok(Some(_)),
4442 ));
4443 assert!(matches!(
4444 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4445 Ok(Some(_)),
4446 ));
4447 assert!(matches!(
4448 PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4449 Ok(None),
4450 ));
4451 assert!(
4453 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4454 );
4455 assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4457 }
4458
4459 #[test]
4460 fn version_request_as_pep440_version() {
4461 assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4463 assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4464 assert_eq!(
4465 VersionRequest::from_str(">=3.10")
4466 .unwrap()
4467 .as_pep440_version(),
4468 None
4469 );
4470
4471 assert_eq!(
4473 VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4474 Some(Version::from_str("3").unwrap())
4475 );
4476
4477 assert_eq!(
4479 VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4480 Some(Version::from_str("3.12").unwrap())
4481 );
4482
4483 assert_eq!(
4485 VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4486 Some(Version::from_str("3.12.5").unwrap())
4487 );
4488
4489 assert_eq!(
4491 VersionRequest::MajorMinorPrerelease(
4492 3,
4493 14,
4494 Prerelease {
4495 kind: PrereleaseKind::Alpha,
4496 number: 1
4497 },
4498 PythonVariant::Default
4499 )
4500 .as_pep440_version(),
4501 Some(Version::from_str("3.14.0a1").unwrap())
4502 );
4503 assert_eq!(
4504 VersionRequest::MajorMinorPrerelease(
4505 3,
4506 14,
4507 Prerelease {
4508 kind: PrereleaseKind::Beta,
4509 number: 2
4510 },
4511 PythonVariant::Default
4512 )
4513 .as_pep440_version(),
4514 Some(Version::from_str("3.14.0b2").unwrap())
4515 );
4516 assert_eq!(
4517 VersionRequest::MajorMinorPrerelease(
4518 3,
4519 13,
4520 Prerelease {
4521 kind: PrereleaseKind::Rc,
4522 number: 3
4523 },
4524 PythonVariant::Default
4525 )
4526 .as_pep440_version(),
4527 Some(Version::from_str("3.13.0rc3").unwrap())
4528 );
4529
4530 assert_eq!(
4532 VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4533 Some(Version::from_str("3").unwrap())
4534 );
4535 assert_eq!(
4536 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4537 Some(Version::from_str("3.13").unwrap())
4538 );
4539 }
4540
4541 #[test]
4542 fn python_request_as_pep440_version() {
4543 assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4545 assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4546
4547 assert_eq!(
4549 PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4550 .as_pep440_version(),
4551 Some(Version::from_str("3.11").unwrap())
4552 );
4553
4554 assert_eq!(
4556 PythonRequest::ImplementationVersion(
4557 ImplementationName::CPython,
4558 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4559 )
4560 .as_pep440_version(),
4561 Some(Version::from_str("3.12.1").unwrap())
4562 );
4563
4564 assert_eq!(
4566 PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4567 None
4568 );
4569
4570 assert_eq!(
4572 PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4573 Some(Version::from_str("3.13.2").unwrap())
4574 );
4575
4576 assert_eq!(
4578 PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4579 None
4580 );
4581
4582 assert_eq!(
4584 PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4585 None
4586 );
4587 }
4588
4589 #[test]
4590 fn intersects_requires_python_exact() {
4591 let requires_python =
4592 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4593
4594 assert!(PythonRequest::parse("3.12").intersects_requires_python(&requires_python));
4595 assert!(!PythonRequest::parse("3.11").intersects_requires_python(&requires_python));
4596 }
4597
4598 #[test]
4599 fn intersects_requires_python_major() {
4600 let requires_python =
4601 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4602
4603 assert!(PythonRequest::parse("3").intersects_requires_python(&requires_python));
4605 assert!(!PythonRequest::parse("2").intersects_requires_python(&requires_python));
4607 }
4608
4609 #[test]
4610 fn intersects_requires_python_range() {
4611 let requires_python =
4612 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4613
4614 assert!(PythonRequest::parse(">=3.12,<3.13").intersects_requires_python(&requires_python));
4615 assert!(!PythonRequest::parse(">=3.10,<3.12").intersects_requires_python(&requires_python));
4616 }
4617
4618 #[test]
4619 fn intersects_requires_python_implementation_range() {
4620 let requires_python =
4621 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4622
4623 assert!(
4624 PythonRequest::parse("cpython@>=3.12,<3.13")
4625 .intersects_requires_python(&requires_python)
4626 );
4627 assert!(
4628 !PythonRequest::parse("cpython@>=3.10,<3.12")
4629 .intersects_requires_python(&requires_python)
4630 );
4631 }
4632
4633 #[test]
4634 fn intersects_requires_python_no_version() {
4635 let requires_python =
4636 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4637
4638 assert!(PythonRequest::Any.intersects_requires_python(&requires_python));
4640 assert!(PythonRequest::Default.intersects_requires_python(&requires_python));
4641 assert!(
4642 PythonRequest::Implementation(ImplementationName::CPython)
4643 .intersects_requires_python(&requires_python)
4644 );
4645 }
4646}