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_fs::Simplified;
17use uv_fs::which::is_executable;
18use uv_pep440::{
19 LowerBound, Prerelease, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
20 release_specifiers_to_ranges,
21};
22use uv_preview::Preview;
23use uv_static::EnvVars;
24use uv_warnings::anstream;
25use uv_warnings::warn_user_once;
26use which::{which, which_all};
27
28use crate::downloads::{ManagedPythonDownloadList, PlatformRequest, PythonDownloadRequest};
29use crate::implementation::ImplementationName;
30use crate::installation::PythonInstallation;
31use crate::interpreter::Error as InterpreterError;
32use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
33use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
34#[cfg(windows)]
35use crate::microsoft_store::find_microsoft_store_pythons;
36use crate::python_version::python_build_versions_from_env;
37use crate::virtualenv::Error as VirtualEnvError;
38use crate::virtualenv::{
39 CondaEnvironmentKind, conda_environment_from_env, virtualenv_from_env,
40 virtualenv_from_working_dir, virtualenv_python_executable,
41};
42#[cfg(windows)]
43use crate::windows_registry::{WindowsPython, registry_pythons};
44use crate::{BrokenSymlink, Interpreter, PythonVersion};
45
46#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
50pub enum PythonRequest {
51 #[default]
56 Default,
57 Any,
59 Version(VersionRequest),
61 Directory(PathBuf),
63 File(PathBuf),
65 ExecutableName(String),
67 Implementation(ImplementationName),
69 ImplementationVersion(ImplementationName, VersionRequest),
71 Key(PythonDownloadRequest),
74}
75
76impl<'a> serde::Deserialize<'a> for PythonRequest {
77 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78 where
79 D: serde::Deserializer<'a>,
80 {
81 let s = <Cow<'_, str>>::deserialize(deserializer)?;
82 Ok(Self::parse(&s))
83 }
84}
85
86impl serde::Serialize for PythonRequest {
87 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88 where
89 S: serde::Serializer,
90 {
91 let s = self.to_canonical_string();
92 serializer.serialize_str(&s)
93 }
94}
95
96#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
97#[serde(deny_unknown_fields, rename_all = "kebab-case")]
98#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
99#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
100pub enum PythonPreference {
101 OnlyManaged,
103 #[default]
104 Managed,
109 System,
113 OnlySystem,
115}
116
117#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
118#[serde(deny_unknown_fields, rename_all = "kebab-case")]
119#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
120#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
121pub enum PythonDownloads {
122 #[default]
124 #[serde(alias = "auto")]
125 Automatic,
126 Manual,
128 Never,
130}
131
132impl FromStr for PythonDownloads {
133 type Err = String;
134
135 fn from_str(s: &str) -> Result<Self, Self::Err> {
136 match s.to_ascii_lowercase().as_str() {
137 "auto" | "automatic" | "true" | "1" => Ok(Self::Automatic),
138 "manual" => Ok(Self::Manual),
139 "never" | "false" | "0" => Ok(Self::Never),
140 _ => Err(format!("Invalid value for `python-download`: '{s}'")),
141 }
142 }
143}
144
145impl From<bool> for PythonDownloads {
146 fn from(value: bool) -> Self {
147 if value { Self::Automatic } else { Self::Never }
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum EnvironmentPreference {
153 #[default]
155 OnlyVirtual,
156 ExplicitSystem,
158 OnlySystem,
160 Any,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Default)]
165pub(crate) struct DiscoveryPreferences {
166 python_preference: PythonPreference,
167 environment_preference: EnvironmentPreference,
168}
169
170#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
171pub enum PythonVariant {
172 #[default]
173 Default,
174 Debug,
175 Freethreaded,
176 FreethreadedDebug,
177 Gil,
178 GilDebug,
179}
180
181#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
183pub enum VersionRequest {
184 #[default]
186 Default,
187 Any,
189 Major(u8, PythonVariant),
190 MajorMinor(u8, u8, PythonVariant),
191 MajorMinorPatch(u8, u8, u8, PythonVariant),
192 MajorMinorPrerelease(u8, u8, Prerelease, PythonVariant),
193 Range(VersionSpecifiers, PythonVariant),
194}
195
196type FindPythonResult = Result<PythonInstallation, PythonNotFound>;
200
201#[derive(Clone, Debug, Error)]
205pub struct PythonNotFound {
206 pub request: PythonRequest,
207 pub python_preference: PythonPreference,
208 pub environment_preference: EnvironmentPreference,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)]
213pub enum PythonSource {
214 ProvidedPath,
216 ActiveEnvironment,
218 CondaPrefix,
220 BaseCondaPrefix,
222 DiscoveredEnvironment,
224 SearchPath,
226 SearchPathFirst,
228 Registry,
230 MicrosoftStore,
232 Managed,
234 ParentInterpreter,
236}
237
238#[derive(Error, Debug)]
239pub enum Error {
240 #[error(transparent)]
241 Io(#[from] io::Error),
242
243 #[error("Failed to inspect Python interpreter from {} at `{}` ", _2, _1.user_display())]
245 Query(
246 #[source] Box<crate::interpreter::Error>,
247 PathBuf,
248 PythonSource,
249 ),
250
251 #[error("Failed to discover managed Python installations")]
254 ManagedPython(#[from] crate::managed::Error),
255
256 #[error(transparent)]
258 VirtualEnv(#[from] crate::virtualenv::Error),
259
260 #[cfg(windows)]
261 #[error("Failed to query installed Python versions from the Windows registry")]
262 RegistryError(#[from] windows::core::Error),
263
264 #[error("Invalid version request: {0}")]
266 InvalidVersionRequest(String),
267
268 #[error("Requesting the 'latest' Python version is not yet supported")]
270 LatestVersionRequest,
271
272 #[error("Interpreter discovery for `{0}` requires `{1}` but only `{2}` is allowed")]
274 SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
275
276 #[error(transparent)]
277 BuildVersion(#[from] crate::python_version::BuildVersionError),
278}
279
280fn python_executables_from_virtual_environments<'a>()
289-> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a {
290 let from_active_environment = iter::once_with(|| {
291 virtualenv_from_env()
292 .into_iter()
293 .map(virtualenv_python_executable)
294 .map(|path| Ok((PythonSource::ActiveEnvironment, path)))
295 })
296 .flatten();
297
298 let from_conda_environment = iter::once_with(|| {
300 conda_environment_from_env(CondaEnvironmentKind::Child)
301 .into_iter()
302 .map(virtualenv_python_executable)
303 .map(|path| Ok((PythonSource::CondaPrefix, path)))
304 })
305 .flatten();
306
307 let from_discovered_environment = iter::once_with(|| {
308 virtualenv_from_working_dir()
309 .map(|path| {
310 path.map(virtualenv_python_executable)
311 .map(|path| (PythonSource::DiscoveredEnvironment, path))
312 .into_iter()
313 })
314 .map_err(Error::from)
315 })
316 .flatten_ok();
317
318 from_active_environment
319 .chain(from_conda_environment)
320 .chain(from_discovered_environment)
321}
322
323fn python_executables_from_installed<'a>(
342 version: &'a VersionRequest,
343 implementation: Option<&'a ImplementationName>,
344 platform: PlatformRequest,
345 preference: PythonPreference,
346 preview: Preview,
347) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
348 let from_managed_installations = iter::once_with(move || {
349 ManagedPythonInstallations::from_settings(None)
350 .map_err(Error::from)
351 .and_then(|installed_installations| {
352 debug!(
353 "Searching for managed installations at `{}`",
354 installed_installations.root().user_display()
355 );
356 let installations = installed_installations.find_matching_current_platform()?;
357
358 let build_versions = python_build_versions_from_env()?;
359
360 Ok(installations
363 .into_iter()
364 .filter(move |installation| {
365 if !version.matches_version(&installation.version()) {
366 debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`");
367 return false;
368 }
369 if !platform.matches(installation.platform()) {
370 debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
371 return false;
372 }
373
374 if let Some(requested_build) = build_versions.get(&installation.implementation()) {
375 let Some(installation_build) = installation.build() else {
376 debug!(
377 "Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation"
378 );
379 return false;
380 };
381 if installation_build != requested_build {
382 debug!(
383 "Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`"
384 );
385 return false;
386 }
387 }
388
389 true
390 })
391 .inspect(|installation| debug!("Found managed installation `{installation}`"))
392 .map(move |installation| {
393 let executable = version
396 .patch()
397 .is_none()
398 .then(|| {
399 PythonMinorVersionLink::from_installation(
400 &installation,
401 preview,
402 )
403 .filter(PythonMinorVersionLink::exists)
404 .map(
405 |minor_version_link| {
406 minor_version_link.symlink_executable.clone()
407 },
408 )
409 })
410 .flatten()
411 .unwrap_or_else(|| installation.executable(false));
412 (PythonSource::Managed, executable)
413 })
414 )
415 })
416 })
417 .flatten_ok();
418
419 let from_search_path = iter::once_with(move || {
420 python_executables_from_search_path(version, implementation)
421 .enumerate()
422 .map(|(i, path)| {
423 if i == 0 {
424 Ok((PythonSource::SearchPathFirst, path))
425 } else {
426 Ok((PythonSource::SearchPath, path))
427 }
428 })
429 })
430 .flatten();
431
432 let from_windows_registry = iter::once_with(move || {
433 #[cfg(windows)]
434 {
435 let version_filter = move |entry: &WindowsPython| {
437 if let Some(found) = &entry.version {
438 if found.string.chars().filter(|c| *c == '.').count() == 1 {
440 version.matches_major_minor(found.major(), found.minor())
441 } else {
442 version.matches_version(found)
443 }
444 } else {
445 true
446 }
447 };
448
449 env::var_os(EnvVars::UV_TEST_PYTHON_PATH)
450 .is_none()
451 .then(|| {
452 registry_pythons()
453 .map(|entries| {
454 entries
455 .into_iter()
456 .filter(version_filter)
457 .map(|entry| (PythonSource::Registry, entry.path))
458 .chain(
459 find_microsoft_store_pythons()
460 .filter(version_filter)
461 .map(|entry| (PythonSource::MicrosoftStore, entry.path)),
462 )
463 })
464 .map_err(Error::from)
465 })
466 .into_iter()
467 .flatten_ok()
468 }
469 #[cfg(not(windows))]
470 {
471 Vec::new()
472 }
473 })
474 .flatten();
475
476 match preference {
477 PythonPreference::OnlyManaged => {
478 if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
482 Box::new(from_managed_installations.chain(from_search_path))
483 } else {
484 Box::new(from_managed_installations)
485 }
486 }
487 PythonPreference::Managed => Box::new(
488 from_managed_installations
489 .chain(from_search_path)
490 .chain(from_windows_registry),
491 ),
492 PythonPreference::System => Box::new(
493 from_search_path
494 .chain(from_windows_registry)
495 .chain(from_managed_installations),
496 ),
497 PythonPreference::OnlySystem => Box::new(from_search_path.chain(from_windows_registry)),
498 }
499}
500
501fn python_executables<'a>(
511 version: &'a VersionRequest,
512 implementation: Option<&'a ImplementationName>,
513 platform: PlatformRequest,
514 environments: EnvironmentPreference,
515 preference: PythonPreference,
516 preview: Preview,
517) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
518 let from_parent_interpreter = iter::once_with(|| {
520 env::var_os(EnvVars::UV_INTERNAL__PARENT_INTERPRETER)
521 .into_iter()
522 .map(|path| Ok((PythonSource::ParentInterpreter, PathBuf::from(path))))
523 })
524 .flatten();
525
526 let from_base_conda_environment = iter::once_with(|| {
528 conda_environment_from_env(CondaEnvironmentKind::Base)
529 .into_iter()
530 .map(virtualenv_python_executable)
531 .map(|path| Ok((PythonSource::BaseCondaPrefix, path)))
532 })
533 .flatten();
534
535 let from_virtual_environments = python_executables_from_virtual_environments();
536 let from_installed =
537 python_executables_from_installed(version, implementation, platform, preference, preview);
538
539 match environments {
543 EnvironmentPreference::OnlyVirtual => {
544 Box::new(from_parent_interpreter.chain(from_virtual_environments))
545 }
546 EnvironmentPreference::ExplicitSystem | EnvironmentPreference::Any => Box::new(
547 from_parent_interpreter
548 .chain(from_virtual_environments)
549 .chain(from_base_conda_environment)
550 .chain(from_installed),
551 ),
552 EnvironmentPreference::OnlySystem => Box::new(
553 from_parent_interpreter
554 .chain(from_base_conda_environment)
555 .chain(from_installed),
556 ),
557 }
558}
559
560fn python_executables_from_search_path<'a>(
572 version: &'a VersionRequest,
573 implementation: Option<&'a ImplementationName>,
574) -> impl Iterator<Item = PathBuf> + 'a {
575 let search_path = env::var_os(EnvVars::UV_TEST_PYTHON_PATH)
577 .unwrap_or(env::var_os(EnvVars::PATH).unwrap_or_default());
578
579 let possible_names: Vec<_> = version
580 .executable_names(implementation)
581 .into_iter()
582 .map(|name| name.to_string())
583 .collect();
584
585 trace!(
586 "Searching PATH for executables: {}",
587 possible_names.join(", ")
588 );
589
590 let search_dirs: Vec<_> = env::split_paths(&search_path).collect();
594 let mut seen_dirs = FxHashSet::with_capacity_and_hasher(search_dirs.len(), FxBuildHasher);
595 search_dirs
596 .into_iter()
597 .filter(|dir| dir.is_dir())
598 .flat_map(move |dir| {
599 let dir_clone = dir.clone();
601 trace!(
602 "Checking `PATH` directory for interpreters: {}",
603 dir.display()
604 );
605 same_file::Handle::from_path(&dir)
606 .map(|handle| seen_dirs.insert(handle))
609 .inspect(|fresh_dir| {
610 if !fresh_dir {
611 trace!("Skipping already seen directory: {}", dir.display());
612 }
613 })
614 .unwrap_or(true)
616 .then(|| {
617 possible_names
618 .clone()
619 .into_iter()
620 .flat_map(move |name| {
621 which::which_in_global(&*name, Some(&dir))
623 .into_iter()
624 .flatten()
625 .collect::<Vec<_>>()
628 })
629 .chain(find_all_minor(implementation, version, &dir_clone))
630 .filter(|path| !is_windows_store_shim(path))
631 .inspect(|path| {
632 trace!("Found possible Python executable: {}", path.display());
633 })
634 .chain(
635 cfg!(windows)
637 .then(move || {
638 which::which_in_global("python.bat", Some(&dir_clone))
639 .into_iter()
640 .flatten()
641 .collect::<Vec<_>>()
642 })
643 .into_iter()
644 .flatten(),
645 )
646 })
647 .into_iter()
648 .flatten()
649 })
650}
651
652fn find_all_minor(
657 implementation: Option<&ImplementationName>,
658 version_request: &VersionRequest,
659 dir: &Path,
660) -> impl Iterator<Item = PathBuf> + use<> {
661 match version_request {
662 &VersionRequest::Any
663 | VersionRequest::Default
664 | VersionRequest::Major(_, _)
665 | VersionRequest::Range(_, _) => {
666 let regex = if let Some(implementation) = implementation {
667 Regex::new(&format!(
668 r"^({}|python3)\.(?<minor>\d\d?)t?{}$",
669 regex::escape(&implementation.to_string()),
670 regex::escape(EXE_SUFFIX)
671 ))
672 .unwrap()
673 } else {
674 Regex::new(&format!(
675 r"^python3\.(?<minor>\d\d?)t?{}$",
676 regex::escape(EXE_SUFFIX)
677 ))
678 .unwrap()
679 };
680 let all_minors = fs_err::read_dir(dir)
681 .into_iter()
682 .flatten()
683 .flatten()
684 .map(|entry| entry.path())
685 .filter(move |path| {
686 let Some(filename) = path.file_name() else {
687 return false;
688 };
689 let Some(filename) = filename.to_str() else {
690 return false;
691 };
692 let Some(captures) = regex.captures(filename) else {
693 return false;
694 };
695
696 let minor = captures["minor"].parse().ok();
698 if let Some(minor) = minor {
699 if minor < 7 {
701 return false;
702 }
703 if !version_request.matches_major_minor(3, minor) {
705 return false;
706 }
707 }
708 true
709 })
710 .filter(|path| is_executable(path))
711 .collect::<Vec<_>>();
712 Either::Left(all_minors.into_iter())
713 }
714 VersionRequest::MajorMinor(_, _, _)
715 | VersionRequest::MajorMinorPatch(_, _, _, _)
716 | VersionRequest::MajorMinorPrerelease(_, _, _, _) => Either::Right(iter::empty()),
717 }
718}
719
720fn python_interpreters<'a>(
730 version: &'a VersionRequest,
731 implementation: Option<&'a ImplementationName>,
732 platform: PlatformRequest,
733 environments: EnvironmentPreference,
734 preference: PythonPreference,
735 cache: &'a Cache,
736 preview: Preview,
737) -> impl Iterator<Item = Result<(PythonSource, Interpreter), Error>> + 'a {
738 let interpreters = python_interpreters_from_executables(
739 python_executables(
743 version,
744 implementation,
745 platform,
746 environments,
747 preference,
748 preview,
749 )
750 .filter_ok(move |(source, path)| {
751 source_satisfies_environment_preference(*source, path, environments)
752 }),
753 cache,
754 )
755 .filter_ok(move |(source, interpreter)| {
756 interpreter_satisfies_environment_preference(*source, interpreter, environments)
757 })
758 .filter_ok(move |(source, interpreter)| {
759 let request = version.clone().into_request_for_source(*source);
760 if request.matches_interpreter(interpreter) {
761 true
762 } else {
763 debug!(
764 "Skipping interpreter at `{}` from {source}: does not satisfy request `{request}`",
765 interpreter.sys_executable().user_display()
766 );
767 false
768 }
769 })
770 .filter_ok(move |(source, interpreter)| {
771 satisfies_python_preference(*source, interpreter, preference)
772 });
773
774 if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
775 Either::Left(interpreters.map_ok(|(source, interpreter)| {
776 if interpreter.is_managed() {
779 (PythonSource::Managed, interpreter)
780 } else {
781 (source, interpreter)
782 }
783 }))
784 } else {
785 Either::Right(interpreters)
786 }
787}
788
789fn python_interpreters_from_executables<'a>(
791 executables: impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
792 cache: &'a Cache,
793) -> impl Iterator<Item = Result<(PythonSource, Interpreter), Error>> + 'a {
794 executables.map(|result| match result {
795 Ok((source, path)) => Interpreter::query(&path, cache)
796 .map(|interpreter| (source, interpreter))
797 .inspect(|(source, interpreter)| {
798 debug!(
799 "Found `{}` at `{}` ({source})",
800 interpreter.key(),
801 path.display()
802 );
803 })
804 .map_err(|err| Error::Query(Box::new(err), path, source))
805 .inspect_err(|err| debug!("{err}")),
806 Err(err) => Err(err),
807 })
808}
809
810fn interpreter_satisfies_environment_preference(
817 source: PythonSource,
818 interpreter: &Interpreter,
819 preference: EnvironmentPreference,
820) -> bool {
821 match (
822 preference,
823 interpreter.is_virtualenv() || (matches!(source, PythonSource::CondaPrefix)),
825 ) {
826 (EnvironmentPreference::Any, _) => true,
827 (EnvironmentPreference::OnlyVirtual, true) => true,
828 (EnvironmentPreference::OnlyVirtual, false) => {
829 debug!(
830 "Ignoring Python interpreter at `{}`: only virtual environments allowed",
831 interpreter.sys_executable().display()
832 );
833 false
834 }
835 (EnvironmentPreference::ExplicitSystem, true) => true,
836 (EnvironmentPreference::ExplicitSystem, false) => {
837 if matches!(
838 source,
839 PythonSource::ProvidedPath | PythonSource::ParentInterpreter
840 ) {
841 debug!(
842 "Allowing explicitly requested system Python interpreter at `{}`",
843 interpreter.sys_executable().display()
844 );
845 true
846 } else {
847 debug!(
848 "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
849 interpreter.sys_executable().display()
850 );
851 false
852 }
853 }
854 (EnvironmentPreference::OnlySystem, true) => {
855 debug!(
856 "Ignoring Python interpreter at `{}`: system interpreter required",
857 interpreter.sys_executable().display()
858 );
859 false
860 }
861 (EnvironmentPreference::OnlySystem, false) => true,
862 }
863}
864
865fn source_satisfies_environment_preference(
872 source: PythonSource,
873 interpreter_path: &Path,
874 preference: EnvironmentPreference,
875) -> bool {
876 match preference {
877 EnvironmentPreference::Any => true,
878 EnvironmentPreference::OnlyVirtual => {
879 if source.is_maybe_virtualenv() {
880 true
881 } else {
882 debug!(
883 "Ignoring Python interpreter at `{}`: only virtual environments allowed",
884 interpreter_path.display()
885 );
886 false
887 }
888 }
889 EnvironmentPreference::ExplicitSystem => {
890 if source.is_maybe_virtualenv() {
891 true
892 } else {
893 debug!(
894 "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
895 interpreter_path.display()
896 );
897 false
898 }
899 }
900 EnvironmentPreference::OnlySystem => {
901 if source.is_maybe_system() {
902 true
903 } else {
904 debug!(
905 "Ignoring Python interpreter at `{}`: system interpreter required",
906 interpreter_path.display()
907 );
908 false
909 }
910 }
911 }
912}
913
914pub fn satisfies_python_preference(
916 source: PythonSource,
917 interpreter: &Interpreter,
918 preference: PythonPreference,
919) -> bool {
920 let is_explicit = match source {
926 PythonSource::ProvidedPath
927 | PythonSource::ParentInterpreter
928 | PythonSource::ActiveEnvironment
929 | PythonSource::CondaPrefix => true,
930 PythonSource::Managed
931 | PythonSource::DiscoveredEnvironment
932 | PythonSource::SearchPath
933 | PythonSource::SearchPathFirst
934 | PythonSource::Registry
935 | PythonSource::MicrosoftStore
936 | PythonSource::BaseCondaPrefix => false,
937 };
938
939 match preference {
940 PythonPreference::OnlyManaged => {
941 if matches!(source, PythonSource::Managed) || interpreter.is_managed() {
943 true
944 } else {
945 if is_explicit {
946 debug!(
947 "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
948 interpreter.sys_executable().display()
949 );
950 true
951 } else {
952 debug!(
953 "Ignoring Python interpreter at `{}`: only managed interpreters allowed",
954 interpreter.sys_executable().display()
955 );
956 false
957 }
958 }
959 }
960 PythonPreference::Managed | PythonPreference::System => true,
962 PythonPreference::OnlySystem => {
963 if is_system_interpreter(source, interpreter) {
964 true
965 } else {
966 if is_explicit {
967 debug!(
968 "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
969 interpreter.sys_executable().display()
970 );
971 true
972 } else {
973 debug!(
974 "Ignoring Python interpreter at `{}`: only system interpreters allowed",
975 interpreter.sys_executable().display()
976 );
977 false
978 }
979 }
980 }
981 }
982}
983
984pub(crate) fn is_system_interpreter(source: PythonSource, interpreter: &Interpreter) -> bool {
985 match source {
986 PythonSource::Managed => false,
988 PythonSource::ProvidedPath
990 | PythonSource::ParentInterpreter
991 | PythonSource::ActiveEnvironment
992 | PythonSource::CondaPrefix
993 | PythonSource::DiscoveredEnvironment
994 | PythonSource::SearchPath
995 | PythonSource::SearchPathFirst
996 | PythonSource::Registry
997 | PythonSource::BaseCondaPrefix => !interpreter.is_managed(),
998 PythonSource::MicrosoftStore => true,
1000 }
1001}
1002
1003impl Error {
1007 pub fn is_critical(&self) -> bool {
1008 match self {
1009 Self::Query(err, _, source) => match &**err {
1012 InterpreterError::Encode(_)
1013 | InterpreterError::Io(_)
1014 | InterpreterError::SpawnFailed { .. } => true,
1015 InterpreterError::UnexpectedResponse(UnexpectedResponseError { path, .. })
1016 | InterpreterError::StatusCode(StatusCodeError { path, .. }) => {
1017 debug!(
1018 "Skipping bad interpreter at {} from {source}: {err}",
1019 path.display()
1020 );
1021 false
1022 }
1023 InterpreterError::QueryScript { path, err } => {
1024 debug!(
1025 "Skipping bad interpreter at {} from {source}: {err}",
1026 path.display()
1027 );
1028 false
1029 }
1030 #[cfg(windows)]
1031 InterpreterError::CorruptWindowsPackage { path, err } => {
1032 debug!(
1033 "Skipping bad interpreter at {} from {source}: {err}",
1034 path.display()
1035 );
1036 false
1037 }
1038 InterpreterError::PermissionDenied { path, err } => {
1039 debug!(
1040 "Skipping unexecutable interpreter at {} from {source}: {err}",
1041 path.display()
1042 );
1043 false
1044 }
1045 InterpreterError::NotFound(path)
1046 | InterpreterError::BrokenSymlink(BrokenSymlink { path, .. }) => {
1047 if matches!(source, PythonSource::ActiveEnvironment)
1050 && uv_fs::is_virtualenv_executable(path)
1051 {
1052 true
1053 } else {
1054 trace!("Skipping missing interpreter at {}", path.display());
1055 false
1056 }
1057 }
1058 },
1059 Self::VirtualEnv(VirtualEnvError::MissingPyVenvCfg(path)) => {
1060 trace!("Skipping broken virtualenv at {}", path.display());
1061 false
1062 }
1063 _ => true,
1064 }
1065 }
1066}
1067
1068fn python_installation_from_executable(
1070 path: &PathBuf,
1071 cache: &Cache,
1072) -> Result<PythonInstallation, crate::interpreter::Error> {
1073 Ok(PythonInstallation {
1074 source: PythonSource::ProvidedPath,
1075 interpreter: Interpreter::query(path, cache)?,
1076 })
1077}
1078
1079fn python_installation_from_directory(
1081 path: &PathBuf,
1082 cache: &Cache,
1083) -> Result<PythonInstallation, crate::interpreter::Error> {
1084 let executable = virtualenv_python_executable(path);
1085 python_installation_from_executable(&executable, cache)
1086}
1087
1088fn python_interpreters_with_executable_name<'a>(
1090 name: &'a str,
1091 cache: &'a Cache,
1092) -> impl Iterator<Item = Result<(PythonSource, Interpreter), Error>> + 'a {
1093 python_interpreters_from_executables(
1094 which_all(name)
1095 .into_iter()
1096 .flat_map(|inner| inner.map(|path| Ok((PythonSource::SearchPath, path)))),
1097 cache,
1098 )
1099}
1100
1101pub fn find_python_installations<'a>(
1103 request: &'a PythonRequest,
1104 environments: EnvironmentPreference,
1105 preference: PythonPreference,
1106 cache: &'a Cache,
1107 preview: Preview,
1108) -> Box<dyn Iterator<Item = Result<FindPythonResult, Error>> + 'a> {
1109 let sources = DiscoveryPreferences {
1110 python_preference: preference,
1111 environment_preference: environments,
1112 }
1113 .sources(request);
1114
1115 match request {
1116 PythonRequest::File(path) => Box::new(iter::once({
1117 if preference.allows(PythonSource::ProvidedPath) {
1118 debug!("Checking for Python interpreter at {request}");
1119 match python_installation_from_executable(path, cache) {
1120 Ok(installation) => Ok(Ok(installation)),
1121 Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
1122 Ok(Err(PythonNotFound {
1123 request: request.clone(),
1124 python_preference: preference,
1125 environment_preference: environments,
1126 }))
1127 }
1128 Err(err) => Err(Error::Query(
1129 Box::new(err),
1130 path.clone(),
1131 PythonSource::ProvidedPath,
1132 )),
1133 }
1134 } else {
1135 Err(Error::SourceNotAllowed(
1136 request.clone(),
1137 PythonSource::ProvidedPath,
1138 preference,
1139 ))
1140 }
1141 })),
1142 PythonRequest::Directory(path) => Box::new(iter::once({
1143 if preference.allows(PythonSource::ProvidedPath) {
1144 debug!("Checking for Python interpreter in {request}");
1145 match python_installation_from_directory(path, cache) {
1146 Ok(installation) => Ok(Ok(installation)),
1147 Err(InterpreterError::NotFound(_) | InterpreterError::BrokenSymlink(_)) => {
1148 Ok(Err(PythonNotFound {
1149 request: request.clone(),
1150 python_preference: preference,
1151 environment_preference: environments,
1152 }))
1153 }
1154 Err(err) => Err(Error::Query(
1155 Box::new(err),
1156 path.clone(),
1157 PythonSource::ProvidedPath,
1158 )),
1159 }
1160 } else {
1161 Err(Error::SourceNotAllowed(
1162 request.clone(),
1163 PythonSource::ProvidedPath,
1164 preference,
1165 ))
1166 }
1167 })),
1168 PythonRequest::ExecutableName(name) => {
1169 if preference.allows(PythonSource::SearchPath) {
1170 debug!("Searching for Python interpreter with {request}");
1171 Box::new(
1172 python_interpreters_with_executable_name(name, cache)
1173 .filter_ok(move |(source, interpreter)| {
1174 interpreter_satisfies_environment_preference(
1175 *source,
1176 interpreter,
1177 environments,
1178 )
1179 })
1180 .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple))),
1181 )
1182 } else {
1183 Box::new(iter::once(Err(Error::SourceNotAllowed(
1184 request.clone(),
1185 PythonSource::SearchPath,
1186 preference,
1187 ))))
1188 }
1189 }
1190 PythonRequest::Any => Box::new({
1191 debug!("Searching for any Python interpreter in {sources}");
1192 python_interpreters(
1193 &VersionRequest::Any,
1194 None,
1195 PlatformRequest::default(),
1196 environments,
1197 preference,
1198 cache,
1199 preview,
1200 )
1201 .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
1202 }),
1203 PythonRequest::Default => Box::new({
1204 debug!("Searching for default Python interpreter in {sources}");
1205 python_interpreters(
1206 &VersionRequest::Default,
1207 None,
1208 PlatformRequest::default(),
1209 environments,
1210 preference,
1211 cache,
1212 preview,
1213 )
1214 .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
1215 }),
1216 PythonRequest::Version(version) => {
1217 if let Err(err) = version.check_supported() {
1218 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1219 }
1220 Box::new({
1221 debug!("Searching for {request} in {sources}");
1222 python_interpreters(
1223 version,
1224 None,
1225 PlatformRequest::default(),
1226 environments,
1227 preference,
1228 cache,
1229 preview,
1230 )
1231 .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
1232 })
1233 }
1234 PythonRequest::Implementation(implementation) => Box::new({
1235 debug!("Searching for a {request} interpreter in {sources}");
1236 python_interpreters(
1237 &VersionRequest::Default,
1238 Some(implementation),
1239 PlatformRequest::default(),
1240 environments,
1241 preference,
1242 cache,
1243 preview,
1244 )
1245 .filter_ok(|(_source, interpreter)| implementation.matches_interpreter(interpreter))
1246 .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
1247 }),
1248 PythonRequest::ImplementationVersion(implementation, version) => {
1249 if let Err(err) = version.check_supported() {
1250 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1251 }
1252 Box::new({
1253 debug!("Searching for {request} in {sources}");
1254 python_interpreters(
1255 version,
1256 Some(implementation),
1257 PlatformRequest::default(),
1258 environments,
1259 preference,
1260 cache,
1261 preview,
1262 )
1263 .filter_ok(|(_source, interpreter)| implementation.matches_interpreter(interpreter))
1264 .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
1265 })
1266 }
1267 PythonRequest::Key(request) => {
1268 if let Some(version) = request.version() {
1269 if let Err(err) = version.check_supported() {
1270 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1271 }
1272 }
1273
1274 Box::new({
1275 debug!("Searching for {request} in {sources}");
1276 python_interpreters(
1277 request.version().unwrap_or(&VersionRequest::Default),
1278 request.implementation(),
1279 request.platform(),
1280 environments,
1281 preference,
1282 cache,
1283 preview,
1284 )
1285 .filter_ok(move |(_source, interpreter)| {
1286 request.satisfied_by_interpreter(interpreter)
1287 })
1288 .map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
1289 })
1290 }
1291 }
1292}
1293
1294pub(crate) fn find_python_installation(
1299 request: &PythonRequest,
1300 environments: EnvironmentPreference,
1301 preference: PythonPreference,
1302 cache: &Cache,
1303 preview: Preview,
1304) -> Result<FindPythonResult, Error> {
1305 let installations =
1306 find_python_installations(request, environments, preference, cache, preview);
1307 let mut first_prerelease = None;
1308 let mut first_debug = None;
1309 let mut first_managed = None;
1310 let mut first_error = None;
1311 for result in installations {
1312 if !result.as_ref().err().is_none_or(Error::is_critical) {
1314 if first_error.is_none() {
1316 if let Err(err) = result {
1317 first_error = Some(err);
1318 }
1319 }
1320 continue;
1321 }
1322
1323 let Ok(Ok(ref installation)) = result else {
1325 return result;
1326 };
1327
1328 let has_default_executable_name = installation.interpreter.has_default_executable_name()
1334 && matches!(
1335 installation.source,
1336 PythonSource::SearchPath | PythonSource::SearchPathFirst
1337 );
1338
1339 if installation.python_version().pre().is_some()
1342 && !request.allows_prereleases()
1343 && !installation.source.allows_prereleases()
1344 && !has_default_executable_name
1345 {
1346 debug!("Skipping pre-release installation {}", installation.key());
1347 if first_prerelease.is_none() {
1348 first_prerelease = Some(installation.clone());
1349 }
1350 continue;
1351 }
1352
1353 if installation.key().variant().is_debug()
1356 && !request.allows_debug()
1357 && !installation.source.allows_debug()
1358 && !has_default_executable_name
1359 {
1360 debug!("Skipping debug installation {}", installation.key());
1361 if first_debug.is_none() {
1362 first_debug = Some(installation.clone());
1363 }
1364 continue;
1365 }
1366
1367 if installation.is_alternative_implementation()
1372 && !request.allows_alternative_implementations()
1373 && !installation.source.allows_alternative_implementations()
1374 && !has_default_executable_name
1375 {
1376 debug!("Skipping alternative implementation {}", installation.key());
1377 continue;
1378 }
1379
1380 if matches!(preference, PythonPreference::System)
1383 && !is_system_interpreter(installation.source, installation.interpreter())
1384 {
1385 debug!(
1386 "Skipping managed installation {}: system installation preferred",
1387 installation.key()
1388 );
1389 if first_managed.is_none() {
1390 first_managed = Some(installation.clone());
1391 }
1392 continue;
1393 }
1394
1395 return result;
1397 }
1398
1399 if let Some(installation) = first_managed {
1402 debug!(
1403 "Allowing managed installation {}: no system installations",
1404 installation.key()
1405 );
1406 return Ok(Ok(installation));
1407 }
1408
1409 if let Some(installation) = first_debug {
1412 debug!(
1413 "Allowing debug installation {}: no non-debug installations",
1414 installation.key()
1415 );
1416 return Ok(Ok(installation));
1417 }
1418
1419 if let Some(installation) = first_prerelease {
1421 debug!(
1422 "Allowing pre-release installation {}: no stable installations",
1423 installation.key()
1424 );
1425 return Ok(Ok(installation));
1426 }
1427
1428 if let Some(err) = first_error {
1431 return Err(err);
1432 }
1433
1434 Ok(Err(PythonNotFound {
1435 request: request.clone(),
1436 environment_preference: environments,
1437 python_preference: preference,
1438 }))
1439}
1440
1441#[instrument(skip_all, fields(request))]
1455pub(crate) async fn find_best_python_installation(
1456 request: &PythonRequest,
1457 environments: EnvironmentPreference,
1458 preference: PythonPreference,
1459 downloads_enabled: bool,
1460 download_list: &ManagedPythonDownloadList,
1461 client: &BaseClient,
1462 retry_policy: &ExponentialBackoff,
1463 cache: &Cache,
1464 reporter: Option<&dyn crate::downloads::Reporter>,
1465 python_install_mirror: Option<&str>,
1466 pypy_install_mirror: Option<&str>,
1467 preview: Preview,
1468) -> Result<PythonInstallation, crate::Error> {
1469 debug!("Starting Python discovery for {request}");
1470 let original_request = request;
1471
1472 let mut previous_fetch_failed = false;
1473
1474 let request_without_patch = match request {
1475 PythonRequest::Version(version) => {
1476 if version.has_patch() {
1477 Some(PythonRequest::Version(version.clone().without_patch()))
1478 } else {
1479 None
1480 }
1481 }
1482 PythonRequest::ImplementationVersion(implementation, version) => Some(
1483 PythonRequest::ImplementationVersion(*implementation, version.clone().without_patch()),
1484 ),
1485 _ => None,
1486 };
1487
1488 for (attempt, request) in iter::once(original_request)
1489 .chain(request_without_patch.iter())
1490 .chain(iter::once(&PythonRequest::Default))
1491 .enumerate()
1492 {
1493 debug!(
1494 "Looking for {request}{}",
1495 if request != original_request {
1496 format!(" attempt {attempt} (fallback after failing to find: {original_request})")
1497 } else {
1498 String::new()
1499 }
1500 );
1501 let result = find_python_installation(request, environments, preference, cache, preview);
1502 let error = match result {
1503 Ok(Ok(installation)) => {
1504 warn_on_unsupported_python(installation.interpreter());
1505 return Ok(installation);
1506 }
1507 Ok(Err(error)) => error.into(),
1509 Err(error) if !error.is_critical() => error.into(),
1510 Err(error) => return Err(error.into()),
1511 };
1512
1513 if downloads_enabled
1515 && !previous_fetch_failed
1516 && let Some(download_request) = PythonDownloadRequest::from_request(request)
1517 {
1518 let download = download_request
1519 .clone()
1520 .fill()
1521 .map(|request| download_list.find(&request));
1522
1523 let result = match download {
1524 Ok(Ok(download)) => PythonInstallation::fetch(
1525 download,
1526 client,
1527 retry_policy,
1528 cache,
1529 reporter,
1530 python_install_mirror,
1531 pypy_install_mirror,
1532 preview,
1533 )
1534 .await
1535 .map(Some),
1536 Ok(Err(crate::downloads::Error::NoDownloadFound(_))) => Ok(None),
1537 Ok(Err(error)) => Err(error.into()),
1538 Err(error) => Err(error.into()),
1539 };
1540 if let Ok(Some(installation)) = result {
1541 return Ok(installation);
1542 }
1543 if let Err(error) = result {
1551 #[derive(Debug, thiserror::Error)]
1553 #[error(
1554 "A managed Python download is available for {0}, but an error occurred when attempting to download it."
1555 )]
1556 struct WrappedError<'a>(&'a PythonRequest, #[source] crate::Error);
1557
1558 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1562 return Err(error);
1563 }
1564
1565 let mut error_chain = String::new();
1566 uv_warnings::write_error_chain(
1568 &WrappedError(request, error),
1569 &mut error_chain,
1570 "warning",
1571 AnsiColors::Yellow,
1572 )
1573 .unwrap();
1574 anstream::eprint!("{}", error_chain);
1575 previous_fetch_failed = true;
1576 }
1577 }
1578
1579 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1585 return Err(match error {
1586 crate::Error::MissingPython(err, _) => PythonNotFound {
1587 request: original_request.clone(),
1589 python_preference: err.python_preference,
1590 environment_preference: err.environment_preference,
1591 }
1592 .into(),
1593 other => other,
1594 });
1595 }
1596 }
1597
1598 unreachable!("The loop should have terminated when it reached PythonRequest::Default");
1599}
1600
1601fn warn_on_unsupported_python(interpreter: &Interpreter) {
1603 if interpreter.python_tuple() < (3, 8) {
1605 warn_user_once!(
1606 "uv is only compatible with Python >=3.8, found Python {}",
1607 interpreter.python_version()
1608 );
1609 }
1610}
1611
1612#[cfg(windows)]
1629pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
1630 use std::os::windows::fs::MetadataExt;
1631 use std::os::windows::prelude::OsStrExt;
1632 use windows::Win32::Foundation::CloseHandle;
1633 use windows::Win32::Storage::FileSystem::{
1634 CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
1635 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_MODE, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
1636 OPEN_EXISTING,
1637 };
1638 use windows::Win32::System::IO::DeviceIoControl;
1639 use windows::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
1640 use windows::core::PCWSTR;
1641
1642 if !path.is_absolute() {
1644 return false;
1645 }
1646
1647 let mut components = path.components().rev();
1650
1651 if !components
1653 .next()
1654 .and_then(|component| component.as_os_str().to_str())
1655 .is_some_and(|component| {
1656 component.starts_with("python")
1657 && std::path::Path::new(component)
1658 .extension()
1659 .is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
1660 })
1661 {
1662 return false;
1663 }
1664
1665 if components
1667 .next()
1668 .is_none_or(|component| component.as_os_str() != "WindowsApps")
1669 {
1670 return false;
1671 }
1672
1673 if components
1675 .next()
1676 .is_none_or(|component| component.as_os_str() != "Microsoft")
1677 {
1678 return false;
1679 }
1680
1681 let Ok(md) = fs_err::symlink_metadata(path) else {
1683 return false;
1684 };
1685 if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 {
1686 return false;
1687 }
1688
1689 let mut path_encoded = path
1690 .as_os_str()
1691 .encode_wide()
1692 .chain(std::iter::once(0))
1693 .collect::<Vec<_>>();
1694
1695 #[allow(unsafe_code)]
1697 let reparse_handle = unsafe {
1698 CreateFileW(
1699 PCWSTR(path_encoded.as_mut_ptr()),
1700 0,
1701 FILE_SHARE_MODE(0),
1702 None,
1703 OPEN_EXISTING,
1704 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
1705 None,
1706 )
1707 };
1708
1709 let Ok(reparse_handle) = reparse_handle else {
1710 return false;
1711 };
1712
1713 let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
1714 let mut bytes_returned = 0;
1715
1716 #[allow(unsafe_code, clippy::cast_possible_truncation)]
1718 let success = unsafe {
1719 DeviceIoControl(
1720 reparse_handle,
1721 FSCTL_GET_REPARSE_POINT,
1722 None,
1723 0,
1724 Some(buf.as_mut_ptr().cast()),
1725 buf.len() as u32 * 2,
1726 Some(&raw mut bytes_returned),
1727 None,
1728 )
1729 .is_ok()
1730 };
1731
1732 #[allow(unsafe_code)]
1734 unsafe {
1735 let _ = CloseHandle(reparse_handle);
1736 }
1737
1738 if !success {
1740 return false;
1741 }
1742
1743 let reparse_point = String::from_utf16_lossy(&buf[..bytes_returned as usize]);
1744 reparse_point.contains("\\AppInstallerPythonRedirector.exe")
1745}
1746
1747#[cfg(not(windows))]
1751fn is_windows_store_shim(_path: &Path) -> bool {
1752 false
1753}
1754
1755impl PythonVariant {
1756 fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
1757 match self {
1758 Self::Default => {
1759 if (interpreter.python_major(), interpreter.python_minor()) >= (3, 14) {
1762 true
1765 } else {
1766 !interpreter.gil_disabled()
1769 }
1770 }
1771 Self::Debug => interpreter.debug_enabled(),
1772 Self::Freethreaded => interpreter.gil_disabled(),
1773 Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
1774 Self::Gil => !interpreter.gil_disabled(),
1775 Self::GilDebug => !interpreter.gil_disabled() && interpreter.debug_enabled(),
1776 }
1777 }
1778
1779 pub fn executable_suffix(self) -> &'static str {
1783 match self {
1784 Self::Default => "",
1785 Self::Debug => "d",
1786 Self::Freethreaded => "t",
1787 Self::FreethreadedDebug => "td",
1788 Self::Gil => "",
1789 Self::GilDebug => "d",
1790 }
1791 }
1792
1793 pub fn display_suffix(self) -> &'static str {
1795 match self {
1796 Self::Default => "",
1797 Self::Debug => "+debug",
1798 Self::Freethreaded => "+freethreaded",
1799 Self::FreethreadedDebug => "+freethreaded+debug",
1800 Self::Gil => "+gil",
1801 Self::GilDebug => "+gil+debug",
1802 }
1803 }
1804
1805 pub fn lib_suffix(self) -> &'static str {
1808 match self {
1809 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "",
1810 Self::Freethreaded | Self::FreethreadedDebug => "t",
1811 }
1812 }
1813
1814 pub fn is_freethreaded(self) -> bool {
1815 match self {
1816 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false,
1817 Self::Freethreaded | Self::FreethreadedDebug => true,
1818 }
1819 }
1820
1821 pub fn is_debug(self) -> bool {
1822 match self {
1823 Self::Default | Self::Freethreaded | Self::Gil => false,
1824 Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true,
1825 }
1826 }
1827}
1828impl PythonRequest {
1829 pub fn parse(value: &str) -> Self {
1837 let lowercase_value = &value.to_ascii_lowercase();
1838
1839 if lowercase_value == "any" {
1841 return Self::Any;
1842 }
1843 if lowercase_value == "default" {
1844 return Self::Default;
1845 }
1846
1847 let abstract_version_prefixes = ["python", ""];
1849 let all_implementation_names =
1850 ImplementationName::long_names().chain(ImplementationName::short_names());
1851 if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1854 abstract_version_prefixes,
1855 all_implementation_names,
1856 lowercase_value,
1857 ) {
1858 return request;
1859 }
1860
1861 let value_as_path = PathBuf::from(value);
1862 if value_as_path.is_dir() {
1864 return Self::Directory(value_as_path);
1865 }
1866 if value_as_path.is_file() {
1868 return Self::File(value_as_path);
1869 }
1870
1871 #[cfg(windows)]
1873 if value_as_path.extension().is_none() {
1874 let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1875 if value_as_path.is_file() {
1876 return Self::File(value_as_path);
1877 }
1878 }
1879
1880 #[cfg(test)]
1885 if value_as_path.is_relative() {
1886 if let Ok(current_dir) = crate::current_dir() {
1887 let relative = current_dir.join(&value_as_path);
1888 if relative.is_dir() {
1889 return Self::Directory(relative);
1890 }
1891 if relative.is_file() {
1892 return Self::File(relative);
1893 }
1894 }
1895 }
1896 if value.contains(std::path::MAIN_SEPARATOR) {
1899 return Self::File(value_as_path);
1900 }
1901 if cfg!(windows) && value.contains('/') {
1904 return Self::File(value_as_path);
1905 }
1906 if let Ok(request) = PythonDownloadRequest::from_str(value) {
1907 return Self::Key(request);
1908 }
1909 Self::ExecutableName(value.to_string())
1912 }
1913
1914 pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
1928 let lowercase_value = &value.to_ascii_lowercase();
1929 let abstract_version_prefixes = if cfg!(windows) {
1931 &["python", "pythonw"][..]
1932 } else {
1933 &["python"][..]
1934 };
1935 if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
1937 return Ok(Some(Self::Default));
1938 }
1939 Self::parse_versions_and_implementations(
1940 abstract_version_prefixes.iter().copied(),
1941 ImplementationName::long_names(),
1942 lowercase_value,
1943 )
1944 }
1945
1946 fn parse_versions_and_implementations<'a>(
1955 abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
1957 implementation_names: impl IntoIterator<Item = &'a str>,
1959 lowercase_value: &str,
1961 ) -> Result<Option<Self>, Error> {
1962 for prefix in abstract_version_prefixes {
1963 if let Some(version_request) =
1964 Self::try_split_prefix_and_version(prefix, lowercase_value)?
1965 {
1966 return Ok(Some(Self::Version(version_request)));
1970 }
1971 }
1972 for implementation in implementation_names {
1973 if lowercase_value == implementation {
1974 return Ok(Some(Self::Implementation(
1975 ImplementationName::from_str(implementation).unwrap(),
1978 )));
1979 }
1980 if let Some(version_request) =
1981 Self::try_split_prefix_and_version(implementation, lowercase_value)?
1982 {
1983 return Ok(Some(Self::ImplementationVersion(
1985 ImplementationName::from_str(implementation).unwrap(),
1987 version_request,
1988 )));
1989 }
1990 }
1991 Ok(None)
1992 }
1993
1994 fn try_split_prefix_and_version(
2005 prefix: &str,
2006 lowercase_value: &str,
2007 ) -> Result<Option<VersionRequest>, Error> {
2008 if lowercase_value.starts_with('@') {
2009 return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
2010 }
2011 let Some(rest) = lowercase_value.strip_prefix(prefix) else {
2012 return Ok(None);
2013 };
2014 if rest.is_empty() {
2016 return Ok(None);
2017 }
2018 if let Some(after_at) = rest.strip_prefix('@') {
2021 if after_at == "latest" {
2022 return Err(Error::LatestVersionRequest);
2025 }
2026 return after_at.parse().map(Some);
2027 }
2028 Ok(rest.parse().ok())
2031 }
2032
2033 pub fn includes_patch(&self) -> bool {
2035 match self {
2036 Self::Default => false,
2037 Self::Any => false,
2038 Self::Version(version_request) => version_request.patch().is_some(),
2039 Self::Directory(..) => false,
2040 Self::File(..) => false,
2041 Self::ExecutableName(..) => false,
2042 Self::Implementation(..) => false,
2043 Self::ImplementationVersion(_, version) => version.patch().is_some(),
2044 Self::Key(request) => request
2045 .version
2046 .as_ref()
2047 .is_some_and(|request| request.patch().is_some()),
2048 }
2049 }
2050
2051 pub fn includes_prerelease(&self) -> bool {
2053 match self {
2054 Self::Default => false,
2055 Self::Any => false,
2056 Self::Version(version_request) => version_request.prerelease().is_some(),
2057 Self::Directory(..) => false,
2058 Self::File(..) => false,
2059 Self::ExecutableName(..) => false,
2060 Self::Implementation(..) => false,
2061 Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
2062 Self::Key(request) => request
2063 .version
2064 .as_ref()
2065 .is_some_and(|request| request.prerelease().is_some()),
2066 }
2067 }
2068
2069 pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
2071 fn is_same_executable(path1: &Path, path2: &Path) -> bool {
2073 path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
2074 }
2075
2076 match self {
2077 Self::Default | Self::Any => true,
2078 Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2079 Self::Directory(directory) => {
2080 is_same_executable(directory, interpreter.sys_prefix())
2082 || is_same_executable(
2083 virtualenv_python_executable(directory).as_path(),
2084 interpreter.sys_executable(),
2085 )
2086 }
2087 Self::File(file) => {
2088 if is_same_executable(interpreter.sys_executable(), file) {
2090 return true;
2091 }
2092 if interpreter
2094 .sys_base_executable()
2095 .is_some_and(|sys_base_executable| {
2096 is_same_executable(sys_base_executable, file)
2097 })
2098 {
2099 return true;
2100 }
2101 if cfg!(windows) {
2106 if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2107 if let (Some(file_base), Some(interpreter_base)) = (
2108 file_interpreter.sys_base_executable(),
2109 interpreter.sys_base_executable(),
2110 ) {
2111 if is_same_executable(file_base, interpreter_base) {
2112 return true;
2113 }
2114 }
2115 }
2116 }
2117 false
2118 }
2119 Self::ExecutableName(name) => {
2120 if interpreter
2122 .sys_executable()
2123 .file_name()
2124 .is_some_and(|filename| filename == name.as_str())
2125 {
2126 return true;
2127 }
2128 if interpreter
2130 .sys_base_executable()
2131 .and_then(|executable| executable.file_name())
2132 .is_some_and(|file_name| file_name == name.as_str())
2133 {
2134 return true;
2135 }
2136 if which(name)
2139 .ok()
2140 .as_ref()
2141 .and_then(|executable| executable.file_name())
2142 .is_some_and(|file_name| file_name == name.as_str())
2143 {
2144 return true;
2145 }
2146 false
2147 }
2148 Self::Implementation(implementation) => interpreter
2149 .implementation_name()
2150 .eq_ignore_ascii_case(implementation.into()),
2151 Self::ImplementationVersion(implementation, version) => {
2152 version.matches_interpreter(interpreter)
2153 && interpreter
2154 .implementation_name()
2155 .eq_ignore_ascii_case(implementation.into())
2156 }
2157 Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2158 }
2159 }
2160
2161 pub(crate) fn allows_prereleases(&self) -> bool {
2163 match self {
2164 Self::Default => false,
2165 Self::Any => true,
2166 Self::Version(version) => version.allows_prereleases(),
2167 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2168 Self::Implementation(_) => false,
2169 Self::ImplementationVersion(_, _) => true,
2170 Self::Key(request) => request.allows_prereleases(),
2171 }
2172 }
2173
2174 pub(crate) fn allows_debug(&self) -> bool {
2176 match self {
2177 Self::Default => false,
2178 Self::Any => true,
2179 Self::Version(version) => version.is_debug(),
2180 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2181 Self::Implementation(_) => false,
2182 Self::ImplementationVersion(_, _) => true,
2183 Self::Key(request) => request.allows_debug(),
2184 }
2185 }
2186
2187 pub(crate) fn allows_alternative_implementations(&self) -> bool {
2189 match self {
2190 Self::Default => false,
2191 Self::Any => true,
2192 Self::Version(_) => false,
2193 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2194 Self::Implementation(implementation)
2195 | Self::ImplementationVersion(implementation, _) => {
2196 !matches!(implementation, ImplementationName::CPython)
2197 }
2198 Self::Key(request) => request.allows_alternative_implementations(),
2199 }
2200 }
2201
2202 pub(crate) fn is_explicit_system(&self) -> bool {
2203 matches!(self, Self::File(_) | Self::Directory(_))
2204 }
2205
2206 pub fn to_canonical_string(&self) -> String {
2210 match self {
2211 Self::Any => "any".to_string(),
2212 Self::Default => "default".to_string(),
2213 Self::Version(version) => version.to_string(),
2214 Self::Directory(path) => path.display().to_string(),
2215 Self::File(path) => path.display().to_string(),
2216 Self::ExecutableName(name) => name.clone(),
2217 Self::Implementation(implementation) => implementation.to_string(),
2218 Self::ImplementationVersion(implementation, version) => {
2219 format!("{implementation}@{version}")
2220 }
2221 Self::Key(request) => request.to_string(),
2222 }
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_maybe_system(self) -> bool {
2306 match self {
2307 Self::CondaPrefix
2308 | Self::BaseCondaPrefix
2309 | Self::ParentInterpreter
2310 | Self::ProvidedPath
2311 | Self::Managed
2312 | Self::SearchPath
2313 | Self::SearchPathFirst
2314 | Self::Registry
2315 | Self::MicrosoftStore => true,
2316 Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2317 }
2318 }
2319}
2320
2321impl PythonPreference {
2322 fn allows(self, source: PythonSource) -> bool {
2323 if !matches!(
2325 source,
2326 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2327 ) {
2328 return true;
2329 }
2330
2331 match self {
2332 Self::OnlyManaged => matches!(source, PythonSource::Managed),
2333 Self::Managed | Self::System => matches!(
2334 source,
2335 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2336 ),
2337 Self::OnlySystem => {
2338 matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2339 }
2340 }
2341 }
2342
2343 pub(crate) fn allows_managed(self) -> bool {
2344 match self {
2345 Self::OnlySystem => false,
2346 Self::Managed | Self::System | Self::OnlyManaged => true,
2347 }
2348 }
2349
2350 #[must_use]
2355 pub fn with_system_flag(self, system: bool) -> Self {
2356 match self {
2357 Self::OnlyManaged => self,
2362 Self::Managed => {
2363 if system {
2364 Self::System
2365 } else {
2366 self
2367 }
2368 }
2369 Self::System => self,
2370 Self::OnlySystem => self,
2371 }
2372 }
2373}
2374
2375impl PythonDownloads {
2376 pub fn is_automatic(self) -> bool {
2377 matches!(self, Self::Automatic)
2378 }
2379}
2380
2381impl EnvironmentPreference {
2382 pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2383 match (system, mutable) {
2384 (true, _) => Self::OnlySystem,
2386 (false, true) => Self::ExplicitSystem,
2388 (false, false) => Self::Any,
2390 }
2391 }
2392}
2393
2394#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2395pub(crate) struct ExecutableName {
2396 implementation: Option<ImplementationName>,
2397 major: Option<u8>,
2398 minor: Option<u8>,
2399 patch: Option<u8>,
2400 prerelease: Option<Prerelease>,
2401 variant: PythonVariant,
2402}
2403
2404#[derive(Debug, Clone, PartialEq, Eq)]
2405struct ExecutableNameComparator<'a> {
2406 name: ExecutableName,
2407 request: &'a VersionRequest,
2408 implementation: Option<&'a ImplementationName>,
2409}
2410
2411impl Ord for ExecutableNameComparator<'_> {
2412 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2416 let name_ordering = if self.implementation.is_some() {
2419 std::cmp::Ordering::Greater
2420 } else {
2421 std::cmp::Ordering::Less
2422 };
2423 if self.name.implementation.is_none() && other.name.implementation.is_some() {
2424 return name_ordering.reverse();
2425 }
2426 if self.name.implementation.is_some() && other.name.implementation.is_none() {
2427 return name_ordering;
2428 }
2429 let ordering = self.name.implementation.cmp(&other.name.implementation);
2431 if ordering != std::cmp::Ordering::Equal {
2432 return ordering;
2433 }
2434 let ordering = self.name.major.cmp(&other.name.major);
2435 let is_default_request =
2436 matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2437 if ordering != std::cmp::Ordering::Equal {
2438 return if is_default_request {
2439 ordering.reverse()
2440 } else {
2441 ordering
2442 };
2443 }
2444 let ordering = self.name.minor.cmp(&other.name.minor);
2445 if ordering != std::cmp::Ordering::Equal {
2446 return if is_default_request {
2447 ordering.reverse()
2448 } else {
2449 ordering
2450 };
2451 }
2452 let ordering = self.name.patch.cmp(&other.name.patch);
2453 if ordering != std::cmp::Ordering::Equal {
2454 return if is_default_request {
2455 ordering.reverse()
2456 } else {
2457 ordering
2458 };
2459 }
2460 let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2461 if ordering != std::cmp::Ordering::Equal {
2462 return if is_default_request {
2463 ordering.reverse()
2464 } else {
2465 ordering
2466 };
2467 }
2468 let ordering = self.name.variant.cmp(&other.name.variant);
2469 if ordering != std::cmp::Ordering::Equal {
2470 return if is_default_request {
2471 ordering.reverse()
2472 } else {
2473 ordering
2474 };
2475 }
2476 ordering
2477 }
2478}
2479
2480impl PartialOrd for ExecutableNameComparator<'_> {
2481 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2482 Some(self.cmp(other))
2483 }
2484}
2485
2486impl ExecutableName {
2487 #[must_use]
2488 fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2489 self.implementation = Some(implementation);
2490 self
2491 }
2492
2493 #[must_use]
2494 fn with_major(mut self, major: u8) -> Self {
2495 self.major = Some(major);
2496 self
2497 }
2498
2499 #[must_use]
2500 fn with_minor(mut self, minor: u8) -> Self {
2501 self.minor = Some(minor);
2502 self
2503 }
2504
2505 #[must_use]
2506 fn with_patch(mut self, patch: u8) -> Self {
2507 self.patch = Some(patch);
2508 self
2509 }
2510
2511 #[must_use]
2512 fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2513 self.prerelease = Some(prerelease);
2514 self
2515 }
2516
2517 #[must_use]
2518 fn with_variant(mut self, variant: PythonVariant) -> Self {
2519 self.variant = variant;
2520 self
2521 }
2522
2523 fn into_comparator<'a>(
2524 self,
2525 request: &'a VersionRequest,
2526 implementation: Option<&'a ImplementationName>,
2527 ) -> ExecutableNameComparator<'a> {
2528 ExecutableNameComparator {
2529 name: self,
2530 request,
2531 implementation,
2532 }
2533 }
2534}
2535
2536impl fmt::Display for ExecutableName {
2537 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2538 if let Some(implementation) = self.implementation {
2539 write!(f, "{implementation}")?;
2540 } else {
2541 f.write_str("python")?;
2542 }
2543 if let Some(major) = self.major {
2544 write!(f, "{major}")?;
2545 if let Some(minor) = self.minor {
2546 write!(f, ".{minor}")?;
2547 if let Some(patch) = self.patch {
2548 write!(f, ".{patch}")?;
2549 }
2550 }
2551 }
2552 if let Some(prerelease) = &self.prerelease {
2553 write!(f, "{prerelease}")?;
2554 }
2555 f.write_str(self.variant.executable_suffix())?;
2556 f.write_str(EXE_SUFFIX)?;
2557 Ok(())
2558 }
2559}
2560
2561impl VersionRequest {
2562 #[must_use]
2564 pub fn only_minor(self) -> Self {
2565 match self {
2566 Self::Any => self,
2567 Self::Default => self,
2568 Self::Range(specifiers, variant) => Self::Range(
2569 specifiers
2570 .into_iter()
2571 .map(|s| s.only_minor_release())
2572 .collect(),
2573 variant,
2574 ),
2575 Self::Major(..) => self,
2576 Self::MajorMinor(..) => self,
2577 Self::MajorMinorPatch(major, minor, _, variant)
2578 | Self::MajorMinorPrerelease(major, minor, _, variant) => {
2579 Self::MajorMinor(major, minor, variant)
2580 }
2581 }
2582 }
2583
2584 pub(crate) fn executable_names(
2586 &self,
2587 implementation: Option<&ImplementationName>,
2588 ) -> Vec<ExecutableName> {
2589 let prerelease = if let Self::MajorMinorPrerelease(_, _, prerelease, _) = self {
2590 Some(prerelease)
2592 } else {
2593 None
2594 };
2595
2596 let mut names = Vec::new();
2598 names.push(ExecutableName::default());
2599
2600 if let Some(major) = self.major() {
2602 names.push(ExecutableName::default().with_major(major));
2604 if let Some(minor) = self.minor() {
2605 names.push(
2607 ExecutableName::default()
2608 .with_major(major)
2609 .with_minor(minor),
2610 );
2611 if let Some(patch) = self.patch() {
2612 names.push(
2614 ExecutableName::default()
2615 .with_major(major)
2616 .with_minor(minor)
2617 .with_patch(patch),
2618 );
2619 }
2620 }
2621 } else {
2622 names.push(ExecutableName::default().with_major(3));
2624 }
2625
2626 if let Some(prerelease) = prerelease {
2627 for i in 0..names.len() {
2629 let name = names[i];
2630 if name.minor.is_none() {
2631 continue;
2634 }
2635 names.push(name.with_prerelease(*prerelease));
2636 }
2637 }
2638
2639 if let Some(implementation) = implementation {
2641 for i in 0..names.len() {
2642 let name = names[i].with_implementation(*implementation);
2643 names.push(name);
2644 }
2645 } else {
2646 if matches!(self, Self::Any) {
2648 for i in 0..names.len() {
2649 for implementation in ImplementationName::iter_all() {
2650 let name = names[i].with_implementation(implementation);
2651 names.push(name);
2652 }
2653 }
2654 }
2655 }
2656
2657 if let Some(variant) = self.variant() {
2659 if variant != PythonVariant::Default {
2660 for i in 0..names.len() {
2661 let name = names[i].with_variant(variant);
2662 names.push(name);
2663 }
2664 }
2665 }
2666
2667 names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
2668 names.reverse();
2669
2670 names
2671 }
2672
2673 pub(crate) fn major(&self) -> Option<u8> {
2675 match self {
2676 Self::Any | Self::Default | Self::Range(_, _) => None,
2677 Self::Major(major, _) => Some(*major),
2678 Self::MajorMinor(major, _, _) => Some(*major),
2679 Self::MajorMinorPatch(major, _, _, _) => Some(*major),
2680 Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2681 }
2682 }
2683
2684 pub(crate) fn minor(&self) -> Option<u8> {
2686 match self {
2687 Self::Any | Self::Default | Self::Range(_, _) => None,
2688 Self::Major(_, _) => None,
2689 Self::MajorMinor(_, minor, _) => Some(*minor),
2690 Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
2691 Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2692 }
2693 }
2694
2695 pub(crate) fn patch(&self) -> Option<u8> {
2697 match self {
2698 Self::Any | Self::Default | Self::Range(_, _) => None,
2699 Self::Major(_, _) => None,
2700 Self::MajorMinor(_, _, _) => None,
2701 Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
2702 Self::MajorMinorPrerelease(_, _, _, _) => None,
2703 }
2704 }
2705
2706 pub(crate) fn prerelease(&self) -> Option<&Prerelease> {
2708 match self {
2709 Self::Any | Self::Default | Self::Range(_, _) => None,
2710 Self::Major(_, _) => None,
2711 Self::MajorMinor(_, _, _) => None,
2712 Self::MajorMinorPatch(_, _, _, _) => None,
2713 Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
2714 }
2715 }
2716
2717 pub(crate) fn check_supported(&self) -> Result<(), String> {
2721 match self {
2722 Self::Any | Self::Default => (),
2723 Self::Major(major, _) => {
2724 if *major < 3 {
2725 return Err(format!(
2726 "Python <3 is not supported but {major} was requested."
2727 ));
2728 }
2729 }
2730 Self::MajorMinor(major, minor, _) => {
2731 if (*major, *minor) < (3, 7) {
2732 return Err(format!(
2733 "Python <3.7 is not supported but {major}.{minor} was requested."
2734 ));
2735 }
2736 }
2737 Self::MajorMinorPatch(major, minor, patch, _) => {
2738 if (*major, *minor) < (3, 7) {
2739 return Err(format!(
2740 "Python <3.7 is not supported but {major}.{minor}.{patch} was requested."
2741 ));
2742 }
2743 }
2744 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2745 if (*major, *minor) < (3, 7) {
2746 return Err(format!(
2747 "Python <3.7 is not supported but {major}.{minor}{prerelease} was requested."
2748 ));
2749 }
2750 }
2751 Self::Range(_, _) => (),
2753 }
2754
2755 if self.is_freethreaded() {
2756 if let Self::MajorMinor(major, minor, _) = self.clone().without_patch() {
2757 if (major, minor) < (3, 13) {
2758 return Err(format!(
2759 "Python <3.13 does not support free-threading but {self} was requested."
2760 ));
2761 }
2762 }
2763 }
2764
2765 Ok(())
2766 }
2767
2768 #[must_use]
2774 pub(crate) fn into_request_for_source(self, source: PythonSource) -> Self {
2775 match self {
2776 Self::Default => match source {
2777 PythonSource::ParentInterpreter
2778 | PythonSource::CondaPrefix
2779 | PythonSource::BaseCondaPrefix
2780 | PythonSource::ProvidedPath
2781 | PythonSource::DiscoveredEnvironment
2782 | PythonSource::ActiveEnvironment => Self::Any,
2783 PythonSource::SearchPath
2784 | PythonSource::SearchPathFirst
2785 | PythonSource::Registry
2786 | PythonSource::MicrosoftStore
2787 | PythonSource::Managed => Self::Default,
2788 },
2789 _ => self,
2790 }
2791 }
2792
2793 pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
2795 match self {
2796 Self::Any => true,
2797 Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
2799 Self::Major(major, variant) => {
2800 interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
2801 }
2802 Self::MajorMinor(major, minor, variant) => {
2803 (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
2804 && variant.matches_interpreter(interpreter)
2805 }
2806 Self::MajorMinorPatch(major, minor, patch, variant) => {
2807 (
2808 interpreter.python_major(),
2809 interpreter.python_minor(),
2810 interpreter.python_patch(),
2811 ) == (*major, *minor, *patch)
2812 && interpreter.python_version().pre().is_none()
2815 && variant.matches_interpreter(interpreter)
2816 }
2817 Self::Range(specifiers, variant) => {
2818 let version = if specifiers
2821 .iter()
2822 .any(uv_pep440::VersionSpecifier::any_prerelease)
2823 {
2824 Cow::Borrowed(interpreter.python_version())
2825 } else {
2826 Cow::Owned(interpreter.python_version().only_release())
2827 };
2828 specifiers.contains(&version) && variant.matches_interpreter(interpreter)
2829 }
2830 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
2831 let version = interpreter.python_version();
2832 let Some(interpreter_prerelease) = version.pre() else {
2833 return false;
2834 };
2835 (
2836 interpreter.python_major(),
2837 interpreter.python_minor(),
2838 interpreter_prerelease,
2839 ) == (*major, *minor, *prerelease)
2840 && variant.matches_interpreter(interpreter)
2841 }
2842 }
2843 }
2844
2845 pub(crate) fn matches_version(&self, version: &PythonVersion) -> bool {
2850 match self {
2851 Self::Any | Self::Default => true,
2852 Self::Major(major, _) => version.major() == *major,
2853 Self::MajorMinor(major, minor, _) => {
2854 (version.major(), version.minor()) == (*major, *minor)
2855 }
2856 Self::MajorMinorPatch(major, minor, patch, _) => {
2857 (version.major(), version.minor(), version.patch())
2858 == (*major, *minor, Some(*patch))
2859 }
2860 Self::Range(specifiers, _) => {
2861 let version = if specifiers
2864 .iter()
2865 .any(uv_pep440::VersionSpecifier::any_prerelease)
2866 {
2867 Cow::Borrowed(&version.version)
2868 } else {
2869 Cow::Owned(version.version.only_release())
2870 };
2871 specifiers.contains(&version)
2872 }
2873 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2874 (version.major(), version.minor(), version.pre())
2875 == (*major, *minor, Some(*prerelease))
2876 }
2877 }
2878 }
2879
2880 fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
2885 match self {
2886 Self::Any | Self::Default => true,
2887 Self::Major(self_major, _) => *self_major == major,
2888 Self::MajorMinor(self_major, self_minor, _) => {
2889 (*self_major, *self_minor) == (major, minor)
2890 }
2891 Self::MajorMinorPatch(self_major, self_minor, _, _) => {
2892 (*self_major, *self_minor) == (major, minor)
2893 }
2894 Self::Range(specifiers, _) => {
2895 let range = release_specifiers_to_ranges(specifiers.clone());
2896 let Some((lower, upper)) = range.bounding_range() else {
2897 return true;
2898 };
2899 let version = Version::new([u64::from(major), u64::from(minor)]);
2900
2901 let lower = LowerBound::new(lower.cloned());
2902 if !lower.major_minor().contains(&version) {
2903 return false;
2904 }
2905
2906 let upper = UpperBound::new(upper.cloned());
2907 if !upper.major_minor().contains(&version) {
2908 return false;
2909 }
2910
2911 true
2912 }
2913 Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
2914 (*self_major, *self_minor) == (major, minor)
2915 }
2916 }
2917 }
2918
2919 pub(crate) fn matches_major_minor_patch_prerelease(
2925 &self,
2926 major: u8,
2927 minor: u8,
2928 patch: u8,
2929 prerelease: Option<Prerelease>,
2930 ) -> bool {
2931 match self {
2932 Self::Any | Self::Default => true,
2933 Self::Major(self_major, _) => *self_major == major,
2934 Self::MajorMinor(self_major, self_minor, _) => {
2935 (*self_major, *self_minor) == (major, minor)
2936 }
2937 Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
2938 (*self_major, *self_minor, *self_patch) == (major, minor, patch)
2939 && prerelease.is_none()
2942 }
2943 Self::Range(specifiers, _) => specifiers.contains(
2944 &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
2945 .with_pre(prerelease),
2946 ),
2947 Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
2948 (*self_major, *self_minor, 0, Some(*self_prerelease))
2950 == (major, minor, patch, prerelease)
2951 }
2952 }
2953 }
2954
2955 fn has_patch(&self) -> bool {
2957 match self {
2958 Self::Any | Self::Default => false,
2959 Self::Major(..) => false,
2960 Self::MajorMinor(..) => false,
2961 Self::MajorMinorPatch(..) => true,
2962 Self::MajorMinorPrerelease(..) => false,
2963 Self::Range(_, _) => false,
2964 }
2965 }
2966
2967 #[must_use]
2971 fn without_patch(self) -> Self {
2972 match self {
2973 Self::Default => Self::Default,
2974 Self::Any => Self::Any,
2975 Self::Major(major, variant) => Self::Major(major, variant),
2976 Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
2977 Self::MajorMinorPatch(major, minor, _, variant) => {
2978 Self::MajorMinor(major, minor, variant)
2979 }
2980 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
2981 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
2982 }
2983 Self::Range(_, _) => self,
2984 }
2985 }
2986
2987 pub(crate) fn allows_prereleases(&self) -> bool {
2989 match self {
2990 Self::Default => false,
2991 Self::Any => true,
2992 Self::Major(..) => false,
2993 Self::MajorMinor(..) => false,
2994 Self::MajorMinorPatch(..) => false,
2995 Self::MajorMinorPrerelease(..) => true,
2996 Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
2997 }
2998 }
2999
3000 pub(crate) fn is_debug(&self) -> bool {
3002 match self {
3003 Self::Any | Self::Default => false,
3004 Self::Major(_, variant)
3005 | Self::MajorMinor(_, _, variant)
3006 | Self::MajorMinorPatch(_, _, _, variant)
3007 | Self::MajorMinorPrerelease(_, _, _, variant)
3008 | Self::Range(_, variant) => variant.is_debug(),
3009 }
3010 }
3011
3012 pub(crate) fn is_freethreaded(&self) -> bool {
3014 match self {
3015 Self::Any | Self::Default => false,
3016 Self::Major(_, variant)
3017 | Self::MajorMinor(_, _, variant)
3018 | Self::MajorMinorPatch(_, _, _, variant)
3019 | Self::MajorMinorPrerelease(_, _, _, variant)
3020 | Self::Range(_, variant) => variant.is_freethreaded(),
3021 }
3022 }
3023
3024 #[must_use]
3028 pub fn without_python_variant(self) -> Self {
3029 match self {
3032 Self::Any | Self::Default => self,
3033 Self::Major(major, _) => Self::Major(major, PythonVariant::Default),
3034 Self::MajorMinor(major, minor, _) => {
3035 Self::MajorMinor(major, minor, PythonVariant::Default)
3036 }
3037 Self::MajorMinorPatch(major, minor, patch, _) => {
3038 Self::MajorMinorPatch(major, minor, patch, PythonVariant::Default)
3039 }
3040 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3041 Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Default)
3042 }
3043 Self::Range(specifiers, _) => Self::Range(specifiers, PythonVariant::Default),
3044 }
3045 }
3046
3047 pub(crate) fn variant(&self) -> Option<PythonVariant> {
3049 match self {
3050 Self::Any => None,
3051 Self::Default => Some(PythonVariant::Default),
3052 Self::Major(_, variant)
3053 | Self::MajorMinor(_, _, variant)
3054 | Self::MajorMinorPatch(_, _, _, variant)
3055 | Self::MajorMinorPrerelease(_, _, _, variant)
3056 | Self::Range(_, variant) => Some(*variant),
3057 }
3058 }
3059}
3060
3061impl FromStr for VersionRequest {
3062 type Err = Error;
3063
3064 fn from_str(s: &str) -> Result<Self, Self::Err> {
3065 fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3068 if s.chars().all(char::is_alphabetic) {
3070 return Err(Error::InvalidVersionRequest(s.to_string()));
3071 }
3072
3073 let Some(mut start) = s.rfind(|c: char| c.is_numeric()) else {
3074 return Ok((s, PythonVariant::Default));
3075 };
3076
3077 start += 1;
3079
3080 if start + 1 > s.len() {
3082 return Ok((s, PythonVariant::Default));
3083 }
3084
3085 let variant = &s[start..];
3086 let prefix = &s[..start];
3087
3088 let variant = variant.strip_prefix('+').unwrap_or(variant);
3090
3091 let Ok(variant) = PythonVariant::from_str(variant) else {
3095 return Ok((s, PythonVariant::Default));
3096 };
3097
3098 Ok((prefix, variant))
3099 }
3100
3101 let (s, variant) = parse_variant(s)?;
3102 let Ok(version) = Version::from_str(s) else {
3103 return parse_version_specifiers_request(s, variant);
3104 };
3105
3106 let version = split_wheel_tag_release_version(version);
3108
3109 if version.post().is_some() || version.dev().is_some() {
3111 return Err(Error::InvalidVersionRequest(s.to_string()));
3112 }
3113
3114 if !version.local().is_empty() {
3117 return Err(Error::InvalidVersionRequest(s.to_string()));
3118 }
3119
3120 let Ok(release) = try_into_u8_slice(&version.release()) else {
3122 return Err(Error::InvalidVersionRequest(s.to_string()));
3123 };
3124
3125 let prerelease = version.pre();
3126
3127 match release.as_slice() {
3128 [major] => {
3130 if prerelease.is_some() {
3132 return Err(Error::InvalidVersionRequest(s.to_string()));
3133 }
3134 Ok(Self::Major(*major, variant))
3135 }
3136 [major, minor] => {
3138 if let Some(prerelease) = prerelease {
3139 return Ok(Self::MajorMinorPrerelease(
3140 *major, *minor, prerelease, variant,
3141 ));
3142 }
3143 Ok(Self::MajorMinor(*major, *minor, variant))
3144 }
3145 [major, minor, patch] => {
3147 if let Some(prerelease) = prerelease {
3148 if *patch != 0 {
3151 return Err(Error::InvalidVersionRequest(s.to_string()));
3152 }
3153 return Ok(Self::MajorMinorPrerelease(
3154 *major, *minor, prerelease, variant,
3155 ));
3156 }
3157 Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3158 }
3159 _ => Err(Error::InvalidVersionRequest(s.to_string())),
3160 }
3161 }
3162}
3163
3164impl FromStr for PythonVariant {
3165 type Err = ();
3166
3167 fn from_str(s: &str) -> Result<Self, Self::Err> {
3168 match s {
3169 "t" | "freethreaded" => Ok(Self::Freethreaded),
3170 "d" | "debug" => Ok(Self::Debug),
3171 "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3172 "gil" => Ok(Self::Gil),
3173 "gil+debug" => Ok(Self::GilDebug),
3174 "" => Ok(Self::Default),
3175 _ => Err(()),
3176 }
3177 }
3178}
3179
3180impl fmt::Display for PythonVariant {
3181 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3182 match self {
3183 Self::Default => f.write_str("default"),
3184 Self::Debug => f.write_str("debug"),
3185 Self::Freethreaded => f.write_str("freethreaded"),
3186 Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3187 Self::Gil => f.write_str("gil"),
3188 Self::GilDebug => f.write_str("gil+debug"),
3189 }
3190 }
3191}
3192
3193fn parse_version_specifiers_request(
3194 s: &str,
3195 variant: PythonVariant,
3196) -> Result<VersionRequest, Error> {
3197 let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3198 return Err(Error::InvalidVersionRequest(s.to_string()));
3199 };
3200 if specifiers.is_empty() {
3201 return Err(Error::InvalidVersionRequest(s.to_string()));
3202 }
3203 Ok(VersionRequest::Range(specifiers, variant))
3204}
3205
3206impl From<&PythonVersion> for VersionRequest {
3207 fn from(version: &PythonVersion) -> Self {
3208 Self::from_str(&version.string)
3209 .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3210 }
3211}
3212
3213impl fmt::Display for VersionRequest {
3214 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3215 match self {
3216 Self::Any => f.write_str("any"),
3217 Self::Default => f.write_str("default"),
3218 Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3219 Self::MajorMinor(major, minor, variant) => {
3220 write!(f, "{major}.{minor}{}", variant.display_suffix())
3221 }
3222 Self::MajorMinorPatch(major, minor, patch, variant) => {
3223 write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3224 }
3225 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3226 write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3227 }
3228 Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3229 }
3230 }
3231}
3232
3233impl fmt::Display for PythonRequest {
3234 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3235 match self {
3236 Self::Default => write!(f, "a default Python"),
3237 Self::Any => write!(f, "any Python"),
3238 Self::Version(version) => write!(f, "Python {version}"),
3239 Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3240 Self::File(path) => write!(f, "path `{}`", path.user_display()),
3241 Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3242 Self::Implementation(implementation) => {
3243 write!(f, "{}", implementation.pretty())
3244 }
3245 Self::ImplementationVersion(implementation, version) => {
3246 write!(f, "{} {version}", implementation.pretty())
3247 }
3248 Self::Key(request) => write!(f, "{request}"),
3249 }
3250 }
3251}
3252
3253impl fmt::Display for PythonSource {
3254 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3255 match self {
3256 Self::ProvidedPath => f.write_str("provided path"),
3257 Self::ActiveEnvironment => f.write_str("active virtual environment"),
3258 Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3259 Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3260 Self::SearchPath => f.write_str("search path"),
3261 Self::SearchPathFirst => f.write_str("first executable in the search path"),
3262 Self::Registry => f.write_str("registry"),
3263 Self::MicrosoftStore => f.write_str("Microsoft Store"),
3264 Self::Managed => f.write_str("managed installations"),
3265 Self::ParentInterpreter => f.write_str("parent interpreter"),
3266 }
3267 }
3268}
3269
3270impl PythonPreference {
3271 fn sources(self) -> &'static [PythonSource] {
3274 match self {
3275 Self::OnlyManaged => &[PythonSource::Managed],
3276 Self::Managed => {
3277 if cfg!(windows) {
3278 &[
3279 PythonSource::Managed,
3280 PythonSource::SearchPath,
3281 PythonSource::Registry,
3282 ]
3283 } else {
3284 &[PythonSource::Managed, PythonSource::SearchPath]
3285 }
3286 }
3287 Self::System => {
3288 if cfg!(windows) {
3289 &[
3290 PythonSource::SearchPath,
3291 PythonSource::Registry,
3292 PythonSource::Managed,
3293 ]
3294 } else {
3295 &[PythonSource::SearchPath, PythonSource::Managed]
3296 }
3297 }
3298 Self::OnlySystem => {
3299 if cfg!(windows) {
3300 &[PythonSource::SearchPath, PythonSource::Registry]
3301 } else {
3302 &[PythonSource::SearchPath]
3303 }
3304 }
3305 }
3306 }
3307
3308 pub fn canonical_name(&self) -> &'static str {
3312 match self {
3313 Self::OnlyManaged => "only managed",
3314 Self::Managed => "prefer managed",
3315 Self::System => "prefer system",
3316 Self::OnlySystem => "only system",
3317 }
3318 }
3319}
3320
3321impl fmt::Display for PythonPreference {
3322 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3323 f.write_str(match self {
3324 Self::OnlyManaged => "only managed",
3325 Self::Managed => "prefer managed",
3326 Self::System => "prefer system",
3327 Self::OnlySystem => "only system",
3328 })
3329 }
3330}
3331
3332impl DiscoveryPreferences {
3333 fn sources(&self, request: &PythonRequest) -> String {
3336 let python_sources = self
3337 .python_preference
3338 .sources()
3339 .iter()
3340 .map(ToString::to_string)
3341 .collect::<Vec<_>>();
3342 match self.environment_preference {
3343 EnvironmentPreference::Any => disjunction(
3344 &["virtual environments"]
3345 .into_iter()
3346 .chain(python_sources.iter().map(String::as_str))
3347 .collect::<Vec<_>>(),
3348 ),
3349 EnvironmentPreference::ExplicitSystem => {
3350 if request.is_explicit_system() {
3351 disjunction(
3352 &["virtual environments"]
3353 .into_iter()
3354 .chain(python_sources.iter().map(String::as_str))
3355 .collect::<Vec<_>>(),
3356 )
3357 } else {
3358 disjunction(&["virtual environments"])
3359 }
3360 }
3361 EnvironmentPreference::OnlySystem => disjunction(
3362 &python_sources
3363 .iter()
3364 .map(String::as_str)
3365 .collect::<Vec<_>>(),
3366 ),
3367 EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3368 }
3369 }
3370}
3371
3372impl fmt::Display for PythonNotFound {
3373 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3374 let sources = DiscoveryPreferences {
3375 python_preference: self.python_preference,
3376 environment_preference: self.environment_preference,
3377 }
3378 .sources(&self.request);
3379
3380 match self.request {
3381 PythonRequest::Default | PythonRequest::Any => {
3382 write!(f, "No interpreter found in {sources}")
3383 }
3384 PythonRequest::File(_) => {
3385 write!(f, "No interpreter found at {}", self.request)
3386 }
3387 PythonRequest::Directory(_) => {
3388 write!(f, "No interpreter found in {}", self.request)
3389 }
3390 _ => {
3391 write!(f, "No interpreter found for {} in {sources}", self.request)
3392 }
3393 }
3394 }
3395}
3396
3397fn disjunction(items: &[&str]) -> String {
3399 match items.len() {
3400 0 => String::new(),
3401 1 => items[0].to_string(),
3402 2 => format!("{} or {}", items[0], items[1]),
3403 _ => {
3404 let last = items.last().unwrap();
3405 format!(
3406 "{}, or {}",
3407 items.iter().take(items.len() - 1).join(", "),
3408 last
3409 )
3410 }
3411 }
3412}
3413
3414fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3415 release
3416 .iter()
3417 .map(|x| match u8::try_from(*x) {
3418 Ok(x) => Ok(x),
3419 Err(e) => Err(e),
3420 })
3421 .collect()
3422}
3423
3424fn split_wheel_tag_release_version(version: Version) -> Version {
3431 let release = version.release();
3432 if release.len() != 1 {
3433 return version;
3434 }
3435
3436 let release = release[0].to_string();
3437 let mut chars = release.chars();
3438 let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3439 return version;
3440 };
3441
3442 let Ok(minor) = chars.as_str().parse::<u32>() else {
3443 return version;
3444 };
3445
3446 version.with_release([u64::from(major), u64::from(minor)])
3447}
3448
3449#[cfg(test)]
3450mod tests {
3451 use std::{path::PathBuf, str::FromStr};
3452
3453 use assert_fs::{TempDir, prelude::*};
3454 use target_lexicon::{Aarch64Architecture, Architecture};
3455 use test_log::test;
3456 use uv_pep440::{Prerelease, PrereleaseKind, VersionSpecifiers};
3457
3458 use crate::{
3459 discovery::{PythonRequest, VersionRequest},
3460 downloads::{ArchRequest, PythonDownloadRequest},
3461 implementation::ImplementationName,
3462 };
3463 use uv_platform::{Arch, Libc, Os};
3464
3465 use super::{
3466 DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonVariant,
3467 };
3468
3469 #[test]
3470 fn interpreter_request_from_str() {
3471 assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3472 assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
3473 assert_eq!(
3474 PythonRequest::parse("3.12"),
3475 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
3476 );
3477 assert_eq!(
3478 PythonRequest::parse(">=3.12"),
3479 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3480 );
3481 assert_eq!(
3482 PythonRequest::parse(">=3.12,<3.13"),
3483 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3484 );
3485 assert_eq!(
3486 PythonRequest::parse(">=3.12,<3.13"),
3487 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3488 );
3489
3490 assert_eq!(
3491 PythonRequest::parse("3.13.0a1"),
3492 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3493 );
3494 assert_eq!(
3495 PythonRequest::parse("3.13.0b5"),
3496 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3497 );
3498 assert_eq!(
3499 PythonRequest::parse("3.13.0rc1"),
3500 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3501 );
3502 assert_eq!(
3503 PythonRequest::parse("3.13.1rc1"),
3504 PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3505 "Pre-release version requests require a patch version of zero"
3506 );
3507 assert_eq!(
3508 PythonRequest::parse("3rc1"),
3509 PythonRequest::ExecutableName("3rc1".to_string()),
3510 "Pre-release version requests require a minor version"
3511 );
3512
3513 assert_eq!(
3514 PythonRequest::parse("cpython"),
3515 PythonRequest::Implementation(ImplementationName::CPython)
3516 );
3517
3518 assert_eq!(
3519 PythonRequest::parse("cpython3.12.2"),
3520 PythonRequest::ImplementationVersion(
3521 ImplementationName::CPython,
3522 VersionRequest::from_str("3.12.2").unwrap(),
3523 )
3524 );
3525
3526 assert_eq!(
3527 PythonRequest::parse("cpython-3.13.2"),
3528 PythonRequest::Key(PythonDownloadRequest {
3529 version: Some(VersionRequest::MajorMinorPatch(
3530 3,
3531 13,
3532 2,
3533 PythonVariant::Default
3534 )),
3535 implementation: Some(ImplementationName::CPython),
3536 arch: None,
3537 os: None,
3538 libc: None,
3539 build: None,
3540 prereleases: None
3541 })
3542 );
3543 assert_eq!(
3544 PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3545 PythonRequest::Key(PythonDownloadRequest {
3546 version: Some(VersionRequest::MajorMinorPatch(
3547 3,
3548 13,
3549 2,
3550 PythonVariant::Default
3551 )),
3552 implementation: Some(ImplementationName::CPython),
3553 arch: Some(ArchRequest::Explicit(Arch::new(
3554 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3555 None
3556 ))),
3557 os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3558 libc: Some(Libc::None),
3559 build: None,
3560 prereleases: None
3561 })
3562 );
3563 assert_eq!(
3564 PythonRequest::parse("any-3.13.2"),
3565 PythonRequest::Key(PythonDownloadRequest {
3566 version: Some(VersionRequest::MajorMinorPatch(
3567 3,
3568 13,
3569 2,
3570 PythonVariant::Default
3571 )),
3572 implementation: None,
3573 arch: None,
3574 os: None,
3575 libc: None,
3576 build: None,
3577 prereleases: None
3578 })
3579 );
3580 assert_eq!(
3581 PythonRequest::parse("any-3.13.2-any-aarch64"),
3582 PythonRequest::Key(PythonDownloadRequest {
3583 version: Some(VersionRequest::MajorMinorPatch(
3584 3,
3585 13,
3586 2,
3587 PythonVariant::Default
3588 )),
3589 implementation: None,
3590 arch: Some(ArchRequest::Explicit(Arch::new(
3591 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3592 None
3593 ))),
3594 os: None,
3595 libc: None,
3596 build: None,
3597 prereleases: None
3598 })
3599 );
3600
3601 assert_eq!(
3602 PythonRequest::parse("pypy"),
3603 PythonRequest::Implementation(ImplementationName::PyPy)
3604 );
3605 assert_eq!(
3606 PythonRequest::parse("pp"),
3607 PythonRequest::Implementation(ImplementationName::PyPy)
3608 );
3609 assert_eq!(
3610 PythonRequest::parse("graalpy"),
3611 PythonRequest::Implementation(ImplementationName::GraalPy)
3612 );
3613 assert_eq!(
3614 PythonRequest::parse("gp"),
3615 PythonRequest::Implementation(ImplementationName::GraalPy)
3616 );
3617 assert_eq!(
3618 PythonRequest::parse("cp"),
3619 PythonRequest::Implementation(ImplementationName::CPython)
3620 );
3621 assert_eq!(
3622 PythonRequest::parse("pypy3.10"),
3623 PythonRequest::ImplementationVersion(
3624 ImplementationName::PyPy,
3625 VersionRequest::from_str("3.10").unwrap(),
3626 )
3627 );
3628 assert_eq!(
3629 PythonRequest::parse("pp310"),
3630 PythonRequest::ImplementationVersion(
3631 ImplementationName::PyPy,
3632 VersionRequest::from_str("3.10").unwrap(),
3633 )
3634 );
3635 assert_eq!(
3636 PythonRequest::parse("graalpy3.10"),
3637 PythonRequest::ImplementationVersion(
3638 ImplementationName::GraalPy,
3639 VersionRequest::from_str("3.10").unwrap(),
3640 )
3641 );
3642 assert_eq!(
3643 PythonRequest::parse("gp310"),
3644 PythonRequest::ImplementationVersion(
3645 ImplementationName::GraalPy,
3646 VersionRequest::from_str("3.10").unwrap(),
3647 )
3648 );
3649 assert_eq!(
3650 PythonRequest::parse("cp38"),
3651 PythonRequest::ImplementationVersion(
3652 ImplementationName::CPython,
3653 VersionRequest::from_str("3.8").unwrap(),
3654 )
3655 );
3656 assert_eq!(
3657 PythonRequest::parse("pypy@3.10"),
3658 PythonRequest::ImplementationVersion(
3659 ImplementationName::PyPy,
3660 VersionRequest::from_str("3.10").unwrap(),
3661 )
3662 );
3663 assert_eq!(
3664 PythonRequest::parse("pypy310"),
3665 PythonRequest::ImplementationVersion(
3666 ImplementationName::PyPy,
3667 VersionRequest::from_str("3.10").unwrap(),
3668 )
3669 );
3670 assert_eq!(
3671 PythonRequest::parse("graalpy@3.10"),
3672 PythonRequest::ImplementationVersion(
3673 ImplementationName::GraalPy,
3674 VersionRequest::from_str("3.10").unwrap(),
3675 )
3676 );
3677 assert_eq!(
3678 PythonRequest::parse("graalpy310"),
3679 PythonRequest::ImplementationVersion(
3680 ImplementationName::GraalPy,
3681 VersionRequest::from_str("3.10").unwrap(),
3682 )
3683 );
3684
3685 let tempdir = TempDir::new().unwrap();
3686 assert_eq!(
3687 PythonRequest::parse(tempdir.path().to_str().unwrap()),
3688 PythonRequest::Directory(tempdir.path().to_path_buf()),
3689 "An existing directory is treated as a directory"
3690 );
3691 assert_eq!(
3692 PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3693 PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3694 "A path that does not exist is treated as a file"
3695 );
3696 tempdir.child("bar").touch().unwrap();
3697 assert_eq!(
3698 PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3699 PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3700 "An existing file is treated as a file"
3701 );
3702 assert_eq!(
3703 PythonRequest::parse("./foo"),
3704 PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
3705 "A string with a file system separator is treated as a file"
3706 );
3707 assert_eq!(
3708 PythonRequest::parse("3.13t"),
3709 PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
3710 );
3711 }
3712
3713 #[test]
3714 fn discovery_sources_prefer_system_orders_search_path_first() {
3715 let preferences = DiscoveryPreferences {
3716 python_preference: PythonPreference::System,
3717 environment_preference: EnvironmentPreference::OnlySystem,
3718 };
3719 let sources = preferences.sources(&PythonRequest::Default);
3720
3721 if cfg!(windows) {
3722 assert_eq!(sources, "search path, registry, or managed installations");
3723 } else {
3724 assert_eq!(sources, "search path or managed installations");
3725 }
3726 }
3727
3728 #[test]
3729 fn discovery_sources_only_system_matches_platform_order() {
3730 let preferences = DiscoveryPreferences {
3731 python_preference: PythonPreference::OnlySystem,
3732 environment_preference: EnvironmentPreference::OnlySystem,
3733 };
3734 let sources = preferences.sources(&PythonRequest::Default);
3735
3736 if cfg!(windows) {
3737 assert_eq!(sources, "search path or registry");
3738 } else {
3739 assert_eq!(sources, "search path");
3740 }
3741 }
3742
3743 #[test]
3744 fn interpreter_request_to_canonical_string() {
3745 assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
3746 assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
3747 assert_eq!(
3748 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
3749 "3.12"
3750 );
3751 assert_eq!(
3752 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3753 .to_canonical_string(),
3754 ">=3.12"
3755 );
3756 assert_eq!(
3757 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3758 .to_canonical_string(),
3759 ">=3.12, <3.13"
3760 );
3761
3762 assert_eq!(
3763 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3764 .to_canonical_string(),
3765 "3.13a1"
3766 );
3767
3768 assert_eq!(
3769 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3770 .to_canonical_string(),
3771 "3.13b5"
3772 );
3773
3774 assert_eq!(
3775 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3776 .to_canonical_string(),
3777 "3.13rc1"
3778 );
3779
3780 assert_eq!(
3781 PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
3782 .to_canonical_string(),
3783 "3.13rc4"
3784 );
3785
3786 assert_eq!(
3787 PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
3788 "foo"
3789 );
3790 assert_eq!(
3791 PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
3792 "cpython"
3793 );
3794 assert_eq!(
3795 PythonRequest::ImplementationVersion(
3796 ImplementationName::CPython,
3797 VersionRequest::from_str("3.12.2").unwrap(),
3798 )
3799 .to_canonical_string(),
3800 "cpython@3.12.2"
3801 );
3802 assert_eq!(
3803 PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
3804 "pypy"
3805 );
3806 assert_eq!(
3807 PythonRequest::ImplementationVersion(
3808 ImplementationName::PyPy,
3809 VersionRequest::from_str("3.10").unwrap(),
3810 )
3811 .to_canonical_string(),
3812 "pypy@3.10"
3813 );
3814 assert_eq!(
3815 PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
3816 "graalpy"
3817 );
3818 assert_eq!(
3819 PythonRequest::ImplementationVersion(
3820 ImplementationName::GraalPy,
3821 VersionRequest::from_str("3.10").unwrap(),
3822 )
3823 .to_canonical_string(),
3824 "graalpy@3.10"
3825 );
3826
3827 let tempdir = TempDir::new().unwrap();
3828 assert_eq!(
3829 PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
3830 tempdir.path().to_str().unwrap(),
3831 "An existing directory is treated as a directory"
3832 );
3833 assert_eq!(
3834 PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
3835 tempdir.child("foo").path().to_str().unwrap(),
3836 "A path that does not exist is treated as a file"
3837 );
3838 tempdir.child("bar").touch().unwrap();
3839 assert_eq!(
3840 PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
3841 tempdir.child("bar").path().to_str().unwrap(),
3842 "An existing file is treated as a file"
3843 );
3844 assert_eq!(
3845 PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
3846 "./foo",
3847 "A string with a file system separator is treated as a file"
3848 );
3849 }
3850
3851 #[test]
3852 fn version_request_from_str() {
3853 assert_eq!(
3854 VersionRequest::from_str("3").unwrap(),
3855 VersionRequest::Major(3, PythonVariant::Default)
3856 );
3857 assert_eq!(
3858 VersionRequest::from_str("3.12").unwrap(),
3859 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
3860 );
3861 assert_eq!(
3862 VersionRequest::from_str("3.12.1").unwrap(),
3863 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
3864 );
3865 assert!(VersionRequest::from_str("1.foo.1").is_err());
3866 assert_eq!(
3867 VersionRequest::from_str("3").unwrap(),
3868 VersionRequest::Major(3, PythonVariant::Default)
3869 );
3870 assert_eq!(
3871 VersionRequest::from_str("38").unwrap(),
3872 VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
3873 );
3874 assert_eq!(
3875 VersionRequest::from_str("312").unwrap(),
3876 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
3877 );
3878 assert_eq!(
3879 VersionRequest::from_str("3100").unwrap(),
3880 VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
3881 );
3882 assert_eq!(
3883 VersionRequest::from_str("3.13a1").unwrap(),
3884 VersionRequest::MajorMinorPrerelease(
3885 3,
3886 13,
3887 Prerelease {
3888 kind: PrereleaseKind::Alpha,
3889 number: 1
3890 },
3891 PythonVariant::Default
3892 )
3893 );
3894 assert_eq!(
3895 VersionRequest::from_str("313b1").unwrap(),
3896 VersionRequest::MajorMinorPrerelease(
3897 3,
3898 13,
3899 Prerelease {
3900 kind: PrereleaseKind::Beta,
3901 number: 1
3902 },
3903 PythonVariant::Default
3904 )
3905 );
3906 assert_eq!(
3907 VersionRequest::from_str("3.13.0b2").unwrap(),
3908 VersionRequest::MajorMinorPrerelease(
3909 3,
3910 13,
3911 Prerelease {
3912 kind: PrereleaseKind::Beta,
3913 number: 2
3914 },
3915 PythonVariant::Default
3916 )
3917 );
3918 assert_eq!(
3919 VersionRequest::from_str("3.13.0rc3").unwrap(),
3920 VersionRequest::MajorMinorPrerelease(
3921 3,
3922 13,
3923 Prerelease {
3924 kind: PrereleaseKind::Rc,
3925 number: 3
3926 },
3927 PythonVariant::Default
3928 )
3929 );
3930 assert!(
3931 matches!(
3932 VersionRequest::from_str("3rc1"),
3933 Err(Error::InvalidVersionRequest(_))
3934 ),
3935 "Pre-release version requests require a minor version"
3936 );
3937 assert!(
3938 matches!(
3939 VersionRequest::from_str("3.13.2rc1"),
3940 Err(Error::InvalidVersionRequest(_))
3941 ),
3942 "Pre-release version requests require a patch version of zero"
3943 );
3944 assert!(
3945 matches!(
3946 VersionRequest::from_str("3.12-dev"),
3947 Err(Error::InvalidVersionRequest(_))
3948 ),
3949 "Development version segments are not allowed"
3950 );
3951 assert!(
3952 matches!(
3953 VersionRequest::from_str("3.12+local"),
3954 Err(Error::InvalidVersionRequest(_))
3955 ),
3956 "Local version segments are not allowed"
3957 );
3958 assert!(
3959 matches!(
3960 VersionRequest::from_str("3.12.post0"),
3961 Err(Error::InvalidVersionRequest(_))
3962 ),
3963 "Post version segments are not allowed"
3964 );
3965 assert!(
3966 matches!(
3968 VersionRequest::from_str("31000"),
3969 Err(Error::InvalidVersionRequest(_))
3970 )
3971 );
3972 assert_eq!(
3973 VersionRequest::from_str("3t").unwrap(),
3974 VersionRequest::Major(3, PythonVariant::Freethreaded)
3975 );
3976 assert_eq!(
3977 VersionRequest::from_str("313t").unwrap(),
3978 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
3979 );
3980 assert_eq!(
3981 VersionRequest::from_str("3.13t").unwrap(),
3982 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
3983 );
3984 assert_eq!(
3985 VersionRequest::from_str(">=3.13t").unwrap(),
3986 VersionRequest::Range(
3987 VersionSpecifiers::from_str(">=3.13").unwrap(),
3988 PythonVariant::Freethreaded
3989 )
3990 );
3991 assert_eq!(
3992 VersionRequest::from_str(">=3.13").unwrap(),
3993 VersionRequest::Range(
3994 VersionSpecifiers::from_str(">=3.13").unwrap(),
3995 PythonVariant::Default
3996 )
3997 );
3998 assert_eq!(
3999 VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4000 VersionRequest::Range(
4001 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4002 PythonVariant::Freethreaded
4003 )
4004 );
4005 assert!(matches!(
4006 VersionRequest::from_str("3.13tt"),
4007 Err(Error::InvalidVersionRequest(_))
4008 ));
4009 }
4010
4011 #[test]
4012 fn executable_names_from_request() {
4013 fn case(request: &str, expected: &[&str]) {
4014 let (implementation, version) = match PythonRequest::parse(request) {
4015 PythonRequest::Any => (None, VersionRequest::Any),
4016 PythonRequest::Default => (None, VersionRequest::Default),
4017 PythonRequest::Version(version) => (None, version),
4018 PythonRequest::ImplementationVersion(implementation, version) => {
4019 (Some(implementation), version)
4020 }
4021 PythonRequest::Implementation(implementation) => {
4022 (Some(implementation), VersionRequest::Default)
4023 }
4024 result => {
4025 panic!("Test cases should request versions or implementations; got {result:?}")
4026 }
4027 };
4028
4029 let result: Vec<_> = version
4030 .executable_names(implementation.as_ref())
4031 .into_iter()
4032 .map(|name| name.to_string())
4033 .collect();
4034
4035 let expected: Vec<_> = expected
4036 .iter()
4037 .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4038 .collect();
4039
4040 assert_eq!(result, expected, "mismatch for case \"{request}\"");
4041 }
4042
4043 case(
4044 "any",
4045 &[
4046 "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4047 "pyodide", "pyodide3",
4048 ],
4049 );
4050
4051 case("default", &["python", "python3"]);
4052
4053 case("3", &["python3", "python"]);
4054
4055 case("4", &["python4", "python"]);
4056
4057 case("3.13", &["python3.13", "python3", "python"]);
4058
4059 case("pypy", &["pypy", "pypy3", "python", "python3"]);
4060
4061 case(
4062 "pypy@3.10",
4063 &[
4064 "pypy3.10",
4065 "pypy3",
4066 "pypy",
4067 "python3.10",
4068 "python3",
4069 "python",
4070 ],
4071 );
4072
4073 case(
4074 "3.13t",
4075 &[
4076 "python3.13t",
4077 "python3.13",
4078 "python3t",
4079 "python3",
4080 "pythont",
4081 "python",
4082 ],
4083 );
4084 case("3t", &["python3t", "python3", "pythont", "python"]);
4085
4086 case(
4087 "3.13.2",
4088 &["python3.13.2", "python3.13", "python3", "python"],
4089 );
4090
4091 case(
4092 "3.13rc2",
4093 &["python3.13rc2", "python3.13", "python3", "python"],
4094 );
4095 }
4096
4097 #[test]
4098 fn test_try_split_prefix_and_version() {
4099 assert!(matches!(
4100 PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4101 Ok(None),
4102 ));
4103 assert!(matches!(
4104 PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4105 Ok(Some(_)),
4106 ));
4107 assert!(matches!(
4108 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4109 Ok(Some(_)),
4110 ));
4111 assert!(matches!(
4112 PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4113 Ok(None),
4114 ));
4115 assert!(
4117 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4118 );
4119 assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4121 }
4122}