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