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