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