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