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::{BrokenSymlink, 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::BrokenSymlink(BrokenSymlink { 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::BrokenSymlink(_)) => {
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::BrokenSymlink(_)) => {
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 #[derive(Debug, thiserror::Error)]
1564 #[error(
1565 "A managed Python download is available for {0}, but an error occurred when attempting to download it."
1566 )]
1567 struct WrappedError<'a>(&'a PythonRequest, #[source] crate::Error);
1568
1569 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1573 return Err(error);
1574 }
1575
1576 let mut error_chain = String::new();
1577 uv_warnings::write_error_chain(
1579 &WrappedError(request, error),
1580 &mut error_chain,
1581 "warning",
1582 AnsiColors::Yellow,
1583 )
1584 .unwrap();
1585 anstream::eprint!("{}", error_chain);
1586 previous_fetch_failed = true;
1587 }
1588 }
1589
1590 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1596 return Err(match error {
1597 crate::Error::MissingPython(err, _) => PythonNotFound {
1598 request: original_request.clone(),
1600 python_preference: err.python_preference,
1601 environment_preference: err.environment_preference,
1602 }
1603 .into(),
1604 other => other,
1605 });
1606 }
1607 }
1608
1609 unreachable!("The loop should have terminated when it reached PythonRequest::Default");
1610}
1611
1612fn warn_on_unsupported_python(interpreter: &Interpreter) {
1614 if interpreter.python_tuple() < (3, 8) {
1616 warn_user_once!(
1617 "uv is only compatible with Python >=3.8, found Python {}",
1618 interpreter.python_version()
1619 );
1620 }
1621}
1622
1623#[cfg(windows)]
1640pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
1641 use std::os::windows::fs::MetadataExt;
1642 use std::os::windows::prelude::OsStrExt;
1643 use windows::Win32::Foundation::CloseHandle;
1644 use windows::Win32::Storage::FileSystem::{
1645 CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
1646 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_MODE, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
1647 OPEN_EXISTING,
1648 };
1649 use windows::Win32::System::IO::DeviceIoControl;
1650 use windows::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
1651 use windows::core::PCWSTR;
1652
1653 if !path.is_absolute() {
1655 return false;
1656 }
1657
1658 let mut components = path.components().rev();
1661
1662 if !components
1664 .next()
1665 .and_then(|component| component.as_os_str().to_str())
1666 .is_some_and(|component| {
1667 component.starts_with("python")
1668 && std::path::Path::new(component)
1669 .extension()
1670 .is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
1671 })
1672 {
1673 return false;
1674 }
1675
1676 if components
1678 .next()
1679 .is_none_or(|component| component.as_os_str() != "WindowsApps")
1680 {
1681 return false;
1682 }
1683
1684 if components
1686 .next()
1687 .is_none_or(|component| component.as_os_str() != "Microsoft")
1688 {
1689 return false;
1690 }
1691
1692 let Ok(md) = fs_err::symlink_metadata(path) else {
1694 return false;
1695 };
1696 if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 {
1697 return false;
1698 }
1699
1700 let mut path_encoded = path
1701 .as_os_str()
1702 .encode_wide()
1703 .chain(std::iter::once(0))
1704 .collect::<Vec<_>>();
1705
1706 #[allow(unsafe_code)]
1708 let reparse_handle = unsafe {
1709 CreateFileW(
1710 PCWSTR(path_encoded.as_mut_ptr()),
1711 0,
1712 FILE_SHARE_MODE(0),
1713 None,
1714 OPEN_EXISTING,
1715 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
1716 None,
1717 )
1718 };
1719
1720 let Ok(reparse_handle) = reparse_handle else {
1721 return false;
1722 };
1723
1724 let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
1725 let mut bytes_returned = 0;
1726
1727 #[allow(unsafe_code, clippy::cast_possible_truncation)]
1729 let success = unsafe {
1730 DeviceIoControl(
1731 reparse_handle,
1732 FSCTL_GET_REPARSE_POINT,
1733 None,
1734 0,
1735 Some(buf.as_mut_ptr().cast()),
1736 buf.len() as u32 * 2,
1737 Some(&raw mut bytes_returned),
1738 None,
1739 )
1740 .is_ok()
1741 };
1742
1743 #[allow(unsafe_code)]
1745 unsafe {
1746 let _ = CloseHandle(reparse_handle);
1747 }
1748
1749 if !success {
1751 return false;
1752 }
1753
1754 let reparse_point = String::from_utf16_lossy(&buf[..bytes_returned as usize]);
1755 reparse_point.contains("\\AppInstallerPythonRedirector.exe")
1756}
1757
1758#[cfg(not(windows))]
1762fn is_windows_store_shim(_path: &Path) -> bool {
1763 false
1764}
1765
1766impl PythonVariant {
1767 fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
1768 match self {
1769 Self::Default => {
1770 if (interpreter.python_major(), interpreter.python_minor()) >= (3, 14) {
1773 true
1776 } else {
1777 !interpreter.gil_disabled()
1780 }
1781 }
1782 Self::Debug => interpreter.debug_enabled(),
1783 Self::Freethreaded => interpreter.gil_disabled(),
1784 Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
1785 Self::Gil => !interpreter.gil_disabled(),
1786 Self::GilDebug => !interpreter.gil_disabled() && interpreter.debug_enabled(),
1787 }
1788 }
1789
1790 pub fn executable_suffix(self) -> &'static str {
1794 match self {
1795 Self::Default => "",
1796 Self::Debug => "d",
1797 Self::Freethreaded => "t",
1798 Self::FreethreadedDebug => "td",
1799 Self::Gil => "",
1800 Self::GilDebug => "d",
1801 }
1802 }
1803
1804 pub fn display_suffix(self) -> &'static str {
1806 match self {
1807 Self::Default => "",
1808 Self::Debug => "+debug",
1809 Self::Freethreaded => "+freethreaded",
1810 Self::FreethreadedDebug => "+freethreaded+debug",
1811 Self::Gil => "+gil",
1812 Self::GilDebug => "+gil+debug",
1813 }
1814 }
1815
1816 pub fn lib_suffix(self) -> &'static str {
1819 match self {
1820 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "",
1821 Self::Freethreaded | Self::FreethreadedDebug => "t",
1822 }
1823 }
1824
1825 pub fn is_freethreaded(self) -> bool {
1826 match self {
1827 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false,
1828 Self::Freethreaded | Self::FreethreadedDebug => true,
1829 }
1830 }
1831
1832 pub fn is_debug(self) -> bool {
1833 match self {
1834 Self::Default | Self::Freethreaded | Self::Gil => false,
1835 Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true,
1836 }
1837 }
1838}
1839impl PythonRequest {
1840 pub fn parse(value: &str) -> Self {
1848 let lowercase_value = &value.to_ascii_lowercase();
1849
1850 if lowercase_value == "any" {
1852 return Self::Any;
1853 }
1854 if lowercase_value == "default" {
1855 return Self::Default;
1856 }
1857
1858 let abstract_version_prefixes = ["python", ""];
1860 let all_implementation_names =
1861 ImplementationName::long_names().chain(ImplementationName::short_names());
1862 if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1865 abstract_version_prefixes,
1866 all_implementation_names,
1867 lowercase_value,
1868 ) {
1869 return request;
1870 }
1871
1872 let value_as_path = PathBuf::from(value);
1873 if value_as_path.is_dir() {
1875 return Self::Directory(value_as_path);
1876 }
1877 if value_as_path.is_file() {
1879 return Self::File(value_as_path);
1880 }
1881
1882 #[cfg(windows)]
1884 if value_as_path.extension().is_none() {
1885 let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1886 if value_as_path.is_file() {
1887 return Self::File(value_as_path);
1888 }
1889 }
1890
1891 #[cfg(test)]
1896 if value_as_path.is_relative() {
1897 if let Ok(current_dir) = crate::current_dir() {
1898 let relative = current_dir.join(&value_as_path);
1899 if relative.is_dir() {
1900 return Self::Directory(relative);
1901 }
1902 if relative.is_file() {
1903 return Self::File(relative);
1904 }
1905 }
1906 }
1907 if value.contains(std::path::MAIN_SEPARATOR) {
1910 return Self::File(value_as_path);
1911 }
1912 if cfg!(windows) && value.contains('/') {
1915 return Self::File(value_as_path);
1916 }
1917 if let Ok(request) = PythonDownloadRequest::from_str(value) {
1918 return Self::Key(request);
1919 }
1920 Self::ExecutableName(value.to_string())
1923 }
1924
1925 pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
1939 let lowercase_value = &value.to_ascii_lowercase();
1940 let abstract_version_prefixes = if cfg!(windows) {
1942 &["python", "pythonw"][..]
1943 } else {
1944 &["python"][..]
1945 };
1946 if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
1948 return Ok(Some(Self::Default));
1949 }
1950 Self::parse_versions_and_implementations(
1951 abstract_version_prefixes.iter().copied(),
1952 ImplementationName::long_names(),
1953 lowercase_value,
1954 )
1955 }
1956
1957 fn parse_versions_and_implementations<'a>(
1966 abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
1968 implementation_names: impl IntoIterator<Item = &'a str>,
1970 lowercase_value: &str,
1972 ) -> Result<Option<Self>, Error> {
1973 for prefix in abstract_version_prefixes {
1974 if let Some(version_request) =
1975 Self::try_split_prefix_and_version(prefix, lowercase_value)?
1976 {
1977 return Ok(Some(Self::Version(version_request)));
1981 }
1982 }
1983 for implementation in implementation_names {
1984 if lowercase_value == implementation {
1985 return Ok(Some(Self::Implementation(
1986 ImplementationName::from_str(implementation).unwrap(),
1989 )));
1990 }
1991 if let Some(version_request) =
1992 Self::try_split_prefix_and_version(implementation, lowercase_value)?
1993 {
1994 return Ok(Some(Self::ImplementationVersion(
1996 ImplementationName::from_str(implementation).unwrap(),
1998 version_request,
1999 )));
2000 }
2001 }
2002 Ok(None)
2003 }
2004
2005 fn try_split_prefix_and_version(
2016 prefix: &str,
2017 lowercase_value: &str,
2018 ) -> Result<Option<VersionRequest>, Error> {
2019 if lowercase_value.starts_with('@') {
2020 return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
2021 }
2022 let Some(rest) = lowercase_value.strip_prefix(prefix) else {
2023 return Ok(None);
2024 };
2025 if rest.is_empty() {
2027 return Ok(None);
2028 }
2029 if let Some(after_at) = rest.strip_prefix('@') {
2032 if after_at == "latest" {
2033 return Err(Error::LatestVersionRequest);
2036 }
2037 return after_at.parse().map(Some);
2038 }
2039 Ok(rest.parse().ok())
2042 }
2043
2044 pub fn includes_patch(&self) -> bool {
2046 match self {
2047 Self::Default => false,
2048 Self::Any => false,
2049 Self::Version(version_request) => version_request.patch().is_some(),
2050 Self::Directory(..) => false,
2051 Self::File(..) => false,
2052 Self::ExecutableName(..) => false,
2053 Self::Implementation(..) => false,
2054 Self::ImplementationVersion(_, version) => version.patch().is_some(),
2055 Self::Key(request) => request
2056 .version
2057 .as_ref()
2058 .is_some_and(|request| request.patch().is_some()),
2059 }
2060 }
2061
2062 pub fn includes_prerelease(&self) -> bool {
2064 match self {
2065 Self::Default => false,
2066 Self::Any => false,
2067 Self::Version(version_request) => version_request.prerelease().is_some(),
2068 Self::Directory(..) => false,
2069 Self::File(..) => false,
2070 Self::ExecutableName(..) => false,
2071 Self::Implementation(..) => false,
2072 Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
2073 Self::Key(request) => request
2074 .version
2075 .as_ref()
2076 .is_some_and(|request| request.prerelease().is_some()),
2077 }
2078 }
2079
2080 pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
2082 fn is_same_executable(path1: &Path, path2: &Path) -> bool {
2084 path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
2085 }
2086
2087 match self {
2088 Self::Default | Self::Any => true,
2089 Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2090 Self::Directory(directory) => {
2091 is_same_executable(directory, interpreter.sys_prefix())
2093 || is_same_executable(
2094 virtualenv_python_executable(directory).as_path(),
2095 interpreter.sys_executable(),
2096 )
2097 }
2098 Self::File(file) => {
2099 if is_same_executable(interpreter.sys_executable(), file) {
2101 return true;
2102 }
2103 if interpreter
2105 .sys_base_executable()
2106 .is_some_and(|sys_base_executable| {
2107 is_same_executable(sys_base_executable, file)
2108 })
2109 {
2110 return true;
2111 }
2112 if cfg!(windows) {
2117 if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2118 if let (Some(file_base), Some(interpreter_base)) = (
2119 file_interpreter.sys_base_executable(),
2120 interpreter.sys_base_executable(),
2121 ) {
2122 if is_same_executable(file_base, interpreter_base) {
2123 return true;
2124 }
2125 }
2126 }
2127 }
2128 false
2129 }
2130 Self::ExecutableName(name) => {
2131 if interpreter
2133 .sys_executable()
2134 .file_name()
2135 .is_some_and(|filename| filename == name.as_str())
2136 {
2137 return true;
2138 }
2139 if interpreter
2141 .sys_base_executable()
2142 .and_then(|executable| executable.file_name())
2143 .is_some_and(|file_name| file_name == name.as_str())
2144 {
2145 return true;
2146 }
2147 if which(name)
2150 .ok()
2151 .as_ref()
2152 .and_then(|executable| executable.file_name())
2153 .is_some_and(|file_name| file_name == name.as_str())
2154 {
2155 return true;
2156 }
2157 false
2158 }
2159 Self::Implementation(implementation) => interpreter
2160 .implementation_name()
2161 .eq_ignore_ascii_case(implementation.into()),
2162 Self::ImplementationVersion(implementation, version) => {
2163 version.matches_interpreter(interpreter)
2164 && interpreter
2165 .implementation_name()
2166 .eq_ignore_ascii_case(implementation.into())
2167 }
2168 Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2169 }
2170 }
2171
2172 pub(crate) fn allows_prereleases(&self) -> bool {
2174 match self {
2175 Self::Default => false,
2176 Self::Any => true,
2177 Self::Version(version) => version.allows_prereleases(),
2178 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2179 Self::Implementation(_) => false,
2180 Self::ImplementationVersion(_, _) => true,
2181 Self::Key(request) => request.allows_prereleases(),
2182 }
2183 }
2184
2185 pub(crate) fn allows_debug(&self) -> bool {
2187 match self {
2188 Self::Default => false,
2189 Self::Any => true,
2190 Self::Version(version) => version.is_debug(),
2191 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2192 Self::Implementation(_) => false,
2193 Self::ImplementationVersion(_, _) => true,
2194 Self::Key(request) => request.allows_debug(),
2195 }
2196 }
2197
2198 pub(crate) fn allows_alternative_implementations(&self) -> bool {
2200 match self {
2201 Self::Default => false,
2202 Self::Any => true,
2203 Self::Version(_) => false,
2204 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2205 Self::Implementation(implementation)
2206 | Self::ImplementationVersion(implementation, _) => {
2207 !matches!(implementation, ImplementationName::CPython)
2208 }
2209 Self::Key(request) => request.allows_alternative_implementations(),
2210 }
2211 }
2212
2213 pub(crate) fn is_explicit_system(&self) -> bool {
2214 matches!(self, Self::File(_) | Self::Directory(_))
2215 }
2216
2217 pub fn to_canonical_string(&self) -> String {
2221 match self {
2222 Self::Any => "any".to_string(),
2223 Self::Default => "default".to_string(),
2224 Self::Version(version) => version.to_string(),
2225 Self::Directory(path) => path.display().to_string(),
2226 Self::File(path) => path.display().to_string(),
2227 Self::ExecutableName(name) => name.clone(),
2228 Self::Implementation(implementation) => implementation.to_string(),
2229 Self::ImplementationVersion(implementation, version) => {
2230 format!("{implementation}@{version}")
2231 }
2232 Self::Key(request) => request.to_string(),
2233 }
2234 }
2235
2236 pub fn as_pep440_version(&self) -> Option<Version> {
2240 match self {
2241 Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2242 Self::Key(download_request) => download_request
2243 .version()
2244 .and_then(VersionRequest::as_pep440_version),
2245 Self::Default
2246 | Self::Any
2247 | Self::Directory(_)
2248 | Self::File(_)
2249 | Self::ExecutableName(_)
2250 | Self::Implementation(_) => None,
2251 }
2252 }
2253
2254 pub fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2260 match self {
2261 Self::Version(version) | Self::ImplementationVersion(_, version) => {
2262 version.as_version_specifiers()
2263 }
2264 Self::Key(download_request) => download_request
2265 .version()
2266 .and_then(VersionRequest::as_version_specifiers),
2267 Self::Default
2268 | Self::Any
2269 | Self::Directory(_)
2270 | Self::File(_)
2271 | Self::ExecutableName(_)
2272 | Self::Implementation(_) => None,
2273 }
2274 }
2275
2276 pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2282 let Some(specifiers) = self.as_version_specifiers() else {
2283 return true;
2284 };
2285
2286 let request_range = release_specifiers_to_ranges(specifiers);
2287 let requires_python_range =
2288 release_specifiers_to_ranges(requires_python.specifiers().clone());
2289 !request_range
2290 .intersection(&requires_python_range)
2291 .is_empty()
2292 }
2293}
2294
2295impl PythonSource {
2296 pub fn is_managed(self) -> bool {
2297 matches!(self, Self::Managed)
2298 }
2299
2300 pub(crate) fn allows_prereleases(self) -> bool {
2302 match self {
2303 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2304 Self::SearchPath
2305 | Self::SearchPathFirst
2306 | Self::CondaPrefix
2307 | Self::BaseCondaPrefix
2308 | Self::ProvidedPath
2309 | Self::ParentInterpreter
2310 | Self::ActiveEnvironment
2311 | Self::DiscoveredEnvironment => true,
2312 }
2313 }
2314
2315 pub(crate) fn allows_debug(self) -> bool {
2317 match self {
2318 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2319 Self::SearchPath
2320 | Self::SearchPathFirst
2321 | Self::CondaPrefix
2322 | Self::BaseCondaPrefix
2323 | Self::ProvidedPath
2324 | Self::ParentInterpreter
2325 | Self::ActiveEnvironment
2326 | Self::DiscoveredEnvironment => true,
2327 }
2328 }
2329
2330 pub(crate) fn allows_alternative_implementations(self) -> bool {
2332 match self {
2333 Self::Managed
2334 | Self::Registry
2335 | Self::SearchPath
2336 | Self::SearchPathFirst
2339 | Self::MicrosoftStore => false,
2340 Self::CondaPrefix
2341 | Self::BaseCondaPrefix
2342 | Self::ProvidedPath
2343 | Self::ParentInterpreter
2344 | Self::ActiveEnvironment
2345 | Self::DiscoveredEnvironment => true,
2346 }
2347 }
2348
2349 pub(crate) fn is_maybe_virtualenv(self) -> bool {
2361 match self {
2362 Self::ProvidedPath
2363 | Self::ActiveEnvironment
2364 | Self::DiscoveredEnvironment
2365 | Self::CondaPrefix
2366 | Self::BaseCondaPrefix
2367 | Self::ParentInterpreter
2368 | Self::SearchPathFirst => true,
2369 Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2370 }
2371 }
2372
2373 pub(crate) fn is_maybe_system(self) -> bool {
2375 match self {
2376 Self::CondaPrefix
2377 | Self::BaseCondaPrefix
2378 | Self::ParentInterpreter
2379 | Self::ProvidedPath
2380 | Self::Managed
2381 | Self::SearchPath
2382 | Self::SearchPathFirst
2383 | Self::Registry
2384 | Self::MicrosoftStore => true,
2385 Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2386 }
2387 }
2388}
2389
2390impl PythonPreference {
2391 fn allows(self, source: PythonSource) -> bool {
2392 if !matches!(
2394 source,
2395 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2396 ) {
2397 return true;
2398 }
2399
2400 match self {
2401 Self::OnlyManaged => matches!(source, PythonSource::Managed),
2402 Self::Managed | Self::System => matches!(
2403 source,
2404 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2405 ),
2406 Self::OnlySystem => {
2407 matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2408 }
2409 }
2410 }
2411
2412 pub(crate) fn allows_managed(self) -> bool {
2413 match self {
2414 Self::OnlySystem => false,
2415 Self::Managed | Self::System | Self::OnlyManaged => true,
2416 }
2417 }
2418
2419 #[must_use]
2424 pub fn with_system_flag(self, system: bool) -> Self {
2425 match self {
2426 Self::OnlyManaged => self,
2431 Self::Managed => {
2432 if system {
2433 Self::System
2434 } else {
2435 self
2436 }
2437 }
2438 Self::System => self,
2439 Self::OnlySystem => self,
2440 }
2441 }
2442}
2443
2444impl PythonDownloads {
2445 pub fn is_automatic(self) -> bool {
2446 matches!(self, Self::Automatic)
2447 }
2448}
2449
2450impl EnvironmentPreference {
2451 pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2452 match (system, mutable) {
2453 (true, _) => Self::OnlySystem,
2455 (false, true) => Self::ExplicitSystem,
2457 (false, false) => Self::Any,
2459 }
2460 }
2461}
2462
2463#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2464pub(crate) struct ExecutableName {
2465 implementation: Option<ImplementationName>,
2466 major: Option<u8>,
2467 minor: Option<u8>,
2468 patch: Option<u8>,
2469 prerelease: Option<Prerelease>,
2470 variant: PythonVariant,
2471}
2472
2473#[derive(Debug, Clone, PartialEq, Eq)]
2474struct ExecutableNameComparator<'a> {
2475 name: ExecutableName,
2476 request: &'a VersionRequest,
2477 implementation: Option<&'a ImplementationName>,
2478}
2479
2480impl Ord for ExecutableNameComparator<'_> {
2481 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2485 let name_ordering = if self.implementation.is_some() {
2488 std::cmp::Ordering::Greater
2489 } else {
2490 std::cmp::Ordering::Less
2491 };
2492 if self.name.implementation.is_none() && other.name.implementation.is_some() {
2493 return name_ordering.reverse();
2494 }
2495 if self.name.implementation.is_some() && other.name.implementation.is_none() {
2496 return name_ordering;
2497 }
2498 let ordering = self.name.implementation.cmp(&other.name.implementation);
2500 if ordering != std::cmp::Ordering::Equal {
2501 return ordering;
2502 }
2503 let ordering = self.name.major.cmp(&other.name.major);
2504 let is_default_request =
2505 matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2506 if ordering != std::cmp::Ordering::Equal {
2507 return if is_default_request {
2508 ordering.reverse()
2509 } else {
2510 ordering
2511 };
2512 }
2513 let ordering = self.name.minor.cmp(&other.name.minor);
2514 if ordering != std::cmp::Ordering::Equal {
2515 return if is_default_request {
2516 ordering.reverse()
2517 } else {
2518 ordering
2519 };
2520 }
2521 let ordering = self.name.patch.cmp(&other.name.patch);
2522 if ordering != std::cmp::Ordering::Equal {
2523 return if is_default_request {
2524 ordering.reverse()
2525 } else {
2526 ordering
2527 };
2528 }
2529 let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2530 if ordering != std::cmp::Ordering::Equal {
2531 return if is_default_request {
2532 ordering.reverse()
2533 } else {
2534 ordering
2535 };
2536 }
2537 let ordering = self.name.variant.cmp(&other.name.variant);
2538 if ordering != std::cmp::Ordering::Equal {
2539 return if is_default_request {
2540 ordering.reverse()
2541 } else {
2542 ordering
2543 };
2544 }
2545 ordering
2546 }
2547}
2548
2549impl PartialOrd for ExecutableNameComparator<'_> {
2550 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2551 Some(self.cmp(other))
2552 }
2553}
2554
2555impl ExecutableName {
2556 #[must_use]
2557 fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2558 self.implementation = Some(implementation);
2559 self
2560 }
2561
2562 #[must_use]
2563 fn with_major(mut self, major: u8) -> Self {
2564 self.major = Some(major);
2565 self
2566 }
2567
2568 #[must_use]
2569 fn with_minor(mut self, minor: u8) -> Self {
2570 self.minor = Some(minor);
2571 self
2572 }
2573
2574 #[must_use]
2575 fn with_patch(mut self, patch: u8) -> Self {
2576 self.patch = Some(patch);
2577 self
2578 }
2579
2580 #[must_use]
2581 fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2582 self.prerelease = Some(prerelease);
2583 self
2584 }
2585
2586 #[must_use]
2587 fn with_variant(mut self, variant: PythonVariant) -> Self {
2588 self.variant = variant;
2589 self
2590 }
2591
2592 fn into_comparator<'a>(
2593 self,
2594 request: &'a VersionRequest,
2595 implementation: Option<&'a ImplementationName>,
2596 ) -> ExecutableNameComparator<'a> {
2597 ExecutableNameComparator {
2598 name: self,
2599 request,
2600 implementation,
2601 }
2602 }
2603}
2604
2605impl fmt::Display for ExecutableName {
2606 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2607 if let Some(implementation) = self.implementation {
2608 write!(f, "{implementation}")?;
2609 } else {
2610 f.write_str("python")?;
2611 }
2612 if let Some(major) = self.major {
2613 write!(f, "{major}")?;
2614 if let Some(minor) = self.minor {
2615 write!(f, ".{minor}")?;
2616 if let Some(patch) = self.patch {
2617 write!(f, ".{patch}")?;
2618 }
2619 }
2620 }
2621 if let Some(prerelease) = &self.prerelease {
2622 write!(f, "{prerelease}")?;
2623 }
2624 f.write_str(self.variant.executable_suffix())?;
2625 f.write_str(EXE_SUFFIX)?;
2626 Ok(())
2627 }
2628}
2629
2630impl VersionRequest {
2631 #[must_use]
2633 pub fn only_minor(self) -> Self {
2634 match self {
2635 Self::Any => self,
2636 Self::Default => self,
2637 Self::Range(specifiers, variant) => Self::Range(
2638 specifiers
2639 .into_iter()
2640 .map(|s| s.only_minor_release())
2641 .collect(),
2642 variant,
2643 ),
2644 Self::Major(..) => self,
2645 Self::MajorMinor(..) => self,
2646 Self::MajorMinorPatch(major, minor, _, variant)
2647 | Self::MajorMinorPrerelease(major, minor, _, variant) => {
2648 Self::MajorMinor(major, minor, variant)
2649 }
2650 }
2651 }
2652
2653 pub(crate) fn executable_names(
2655 &self,
2656 implementation: Option<&ImplementationName>,
2657 ) -> Vec<ExecutableName> {
2658 let prerelease = if let Self::MajorMinorPrerelease(_, _, prerelease, _) = self {
2659 Some(prerelease)
2661 } else {
2662 None
2663 };
2664
2665 let mut names = Vec::new();
2667 names.push(ExecutableName::default());
2668
2669 if let Some(major) = self.major() {
2671 names.push(ExecutableName::default().with_major(major));
2673 if let Some(minor) = self.minor() {
2674 names.push(
2676 ExecutableName::default()
2677 .with_major(major)
2678 .with_minor(minor),
2679 );
2680 if let Some(patch) = self.patch() {
2681 names.push(
2683 ExecutableName::default()
2684 .with_major(major)
2685 .with_minor(minor)
2686 .with_patch(patch),
2687 );
2688 }
2689 }
2690 } else {
2691 names.push(ExecutableName::default().with_major(3));
2693 }
2694
2695 if let Some(prerelease) = prerelease {
2696 for i in 0..names.len() {
2698 let name = names[i];
2699 if name.minor.is_none() {
2700 continue;
2703 }
2704 names.push(name.with_prerelease(*prerelease));
2705 }
2706 }
2707
2708 if let Some(implementation) = implementation {
2710 for i in 0..names.len() {
2711 let name = names[i].with_implementation(*implementation);
2712 names.push(name);
2713 }
2714 } else {
2715 if matches!(self, Self::Any) {
2717 for i in 0..names.len() {
2718 for implementation in ImplementationName::iter_all() {
2719 let name = names[i].with_implementation(implementation);
2720 names.push(name);
2721 }
2722 }
2723 }
2724 }
2725
2726 if let Some(variant) = self.variant() {
2728 if variant != PythonVariant::Default {
2729 for i in 0..names.len() {
2730 let name = names[i].with_variant(variant);
2731 names.push(name);
2732 }
2733 }
2734 }
2735
2736 names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
2737 names.reverse();
2738
2739 names
2740 }
2741
2742 pub(crate) fn major(&self) -> Option<u8> {
2744 match self {
2745 Self::Any | Self::Default | Self::Range(_, _) => None,
2746 Self::Major(major, _) => Some(*major),
2747 Self::MajorMinor(major, _, _) => Some(*major),
2748 Self::MajorMinorPatch(major, _, _, _) => Some(*major),
2749 Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2750 }
2751 }
2752
2753 pub(crate) fn minor(&self) -> Option<u8> {
2755 match self {
2756 Self::Any | Self::Default | Self::Range(_, _) => None,
2757 Self::Major(_, _) => None,
2758 Self::MajorMinor(_, minor, _) => Some(*minor),
2759 Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
2760 Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2761 }
2762 }
2763
2764 pub(crate) fn patch(&self) -> Option<u8> {
2766 match self {
2767 Self::Any | Self::Default | Self::Range(_, _) => None,
2768 Self::Major(_, _) => None,
2769 Self::MajorMinor(_, _, _) => None,
2770 Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
2771 Self::MajorMinorPrerelease(_, _, _, _) => None,
2772 }
2773 }
2774
2775 pub(crate) fn prerelease(&self) -> Option<&Prerelease> {
2777 match self {
2778 Self::Any | Self::Default | Self::Range(_, _) => None,
2779 Self::Major(_, _) => None,
2780 Self::MajorMinor(_, _, _) => None,
2781 Self::MajorMinorPatch(_, _, _, _) => None,
2782 Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
2783 }
2784 }
2785
2786 pub(crate) fn check_supported(&self) -> Result<(), String> {
2790 match self {
2791 Self::Any | Self::Default => (),
2792 Self::Major(major, _) => {
2793 if *major < 3 {
2794 return Err(format!(
2795 "Python <3 is not supported but {major} was requested."
2796 ));
2797 }
2798 }
2799 Self::MajorMinor(major, minor, _) => {
2800 if (*major, *minor) < (3, 7) {
2801 return Err(format!(
2802 "Python <3.7 is not supported but {major}.{minor} was requested."
2803 ));
2804 }
2805 }
2806 Self::MajorMinorPatch(major, minor, patch, _) => {
2807 if (*major, *minor) < (3, 7) {
2808 return Err(format!(
2809 "Python <3.7 is not supported but {major}.{minor}.{patch} was requested."
2810 ));
2811 }
2812 }
2813 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2814 if (*major, *minor) < (3, 7) {
2815 return Err(format!(
2816 "Python <3.7 is not supported but {major}.{minor}{prerelease} was requested."
2817 ));
2818 }
2819 }
2820 Self::Range(_, _) => (),
2822 }
2823
2824 if self.is_freethreaded() {
2825 if let Self::MajorMinor(major, minor, _) = self.clone().without_patch() {
2826 if (major, minor) < (3, 13) {
2827 return Err(format!(
2828 "Python <3.13 does not support free-threading but {self} was requested."
2829 ));
2830 }
2831 }
2832 }
2833
2834 Ok(())
2835 }
2836
2837 #[must_use]
2843 pub(crate) fn into_request_for_source(self, source: PythonSource) -> Self {
2844 match self {
2845 Self::Default => match source {
2846 PythonSource::ParentInterpreter
2847 | PythonSource::CondaPrefix
2848 | PythonSource::BaseCondaPrefix
2849 | PythonSource::ProvidedPath
2850 | PythonSource::DiscoveredEnvironment
2851 | PythonSource::ActiveEnvironment => Self::Any,
2852 PythonSource::SearchPath
2853 | PythonSource::SearchPathFirst
2854 | PythonSource::Registry
2855 | PythonSource::MicrosoftStore
2856 | PythonSource::Managed => Self::Default,
2857 },
2858 _ => self,
2859 }
2860 }
2861
2862 pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
2864 match self {
2865 Self::Any => true,
2866 Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
2868 Self::Major(major, variant) => {
2869 interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
2870 }
2871 Self::MajorMinor(major, minor, variant) => {
2872 (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
2873 && variant.matches_interpreter(interpreter)
2874 }
2875 Self::MajorMinorPatch(major, minor, patch, variant) => {
2876 (
2877 interpreter.python_major(),
2878 interpreter.python_minor(),
2879 interpreter.python_patch(),
2880 ) == (*major, *minor, *patch)
2881 && interpreter.python_version().pre().is_none()
2884 && variant.matches_interpreter(interpreter)
2885 }
2886 Self::Range(specifiers, variant) => {
2887 let version = if specifiers
2890 .iter()
2891 .any(uv_pep440::VersionSpecifier::any_prerelease)
2892 {
2893 Cow::Borrowed(interpreter.python_version())
2894 } else {
2895 Cow::Owned(interpreter.python_version().only_release())
2896 };
2897 specifiers.contains(&version) && variant.matches_interpreter(interpreter)
2898 }
2899 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
2900 let version = interpreter.python_version();
2901 let Some(interpreter_prerelease) = version.pre() else {
2902 return false;
2903 };
2904 (
2905 interpreter.python_major(),
2906 interpreter.python_minor(),
2907 interpreter_prerelease,
2908 ) == (*major, *minor, *prerelease)
2909 && variant.matches_interpreter(interpreter)
2910 }
2911 }
2912 }
2913
2914 pub(crate) fn matches_version(&self, version: &PythonVersion) -> bool {
2919 match self {
2920 Self::Any | Self::Default => true,
2921 Self::Major(major, _) => version.major() == *major,
2922 Self::MajorMinor(major, minor, _) => {
2923 (version.major(), version.minor()) == (*major, *minor)
2924 }
2925 Self::MajorMinorPatch(major, minor, patch, _) => {
2926 (version.major(), version.minor(), version.patch())
2927 == (*major, *minor, Some(*patch))
2928 }
2929 Self::Range(specifiers, _) => {
2930 let version = if specifiers
2933 .iter()
2934 .any(uv_pep440::VersionSpecifier::any_prerelease)
2935 {
2936 Cow::Borrowed(&version.version)
2937 } else {
2938 Cow::Owned(version.version.only_release())
2939 };
2940 specifiers.contains(&version)
2941 }
2942 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2943 (version.major(), version.minor(), version.pre())
2944 == (*major, *minor, Some(*prerelease))
2945 }
2946 }
2947 }
2948
2949 fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
2954 match self {
2955 Self::Any | Self::Default => true,
2956 Self::Major(self_major, _) => *self_major == major,
2957 Self::MajorMinor(self_major, self_minor, _) => {
2958 (*self_major, *self_minor) == (major, minor)
2959 }
2960 Self::MajorMinorPatch(self_major, self_minor, _, _) => {
2961 (*self_major, *self_minor) == (major, minor)
2962 }
2963 Self::Range(specifiers, _) => {
2964 let range = release_specifiers_to_ranges(specifiers.clone());
2965 let Some((lower, upper)) = range.bounding_range() else {
2966 return true;
2967 };
2968 let version = Version::new([u64::from(major), u64::from(minor)]);
2969
2970 let lower = LowerBound::new(lower.cloned());
2971 if !lower.major_minor().contains(&version) {
2972 return false;
2973 }
2974
2975 let upper = UpperBound::new(upper.cloned());
2976 if !upper.major_minor().contains(&version) {
2977 return false;
2978 }
2979
2980 true
2981 }
2982 Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
2983 (*self_major, *self_minor) == (major, minor)
2984 }
2985 }
2986 }
2987
2988 pub(crate) fn matches_major_minor_patch_prerelease(
2994 &self,
2995 major: u8,
2996 minor: u8,
2997 patch: u8,
2998 prerelease: Option<Prerelease>,
2999 ) -> bool {
3000 match self {
3001 Self::Any | Self::Default => true,
3002 Self::Major(self_major, _) => *self_major == major,
3003 Self::MajorMinor(self_major, self_minor, _) => {
3004 (*self_major, *self_minor) == (major, minor)
3005 }
3006 Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
3007 (*self_major, *self_minor, *self_patch) == (major, minor, patch)
3008 && prerelease.is_none()
3011 }
3012 Self::Range(specifiers, _) => specifiers.contains(
3013 &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
3014 .with_pre(prerelease),
3015 ),
3016 Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3017 (*self_major, *self_minor, 0, Some(*self_prerelease))
3019 == (major, minor, patch, prerelease)
3020 }
3021 }
3022 }
3023
3024 pub(crate) fn matches_installation_key(&self, key: &PythonInstallationKey) -> bool {
3029 self.matches_major_minor_patch_prerelease(key.major, key.minor, key.patch, key.prerelease())
3030 }
3031
3032 fn has_patch(&self) -> bool {
3034 match self {
3035 Self::Any | Self::Default => false,
3036 Self::Major(..) => false,
3037 Self::MajorMinor(..) => false,
3038 Self::MajorMinorPatch(..) => true,
3039 Self::MajorMinorPrerelease(..) => false,
3040 Self::Range(_, _) => false,
3041 }
3042 }
3043
3044 #[must_use]
3048 fn without_patch(self) -> Self {
3049 match self {
3050 Self::Default => Self::Default,
3051 Self::Any => Self::Any,
3052 Self::Major(major, variant) => Self::Major(major, variant),
3053 Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3054 Self::MajorMinorPatch(major, minor, _, variant) => {
3055 Self::MajorMinor(major, minor, variant)
3056 }
3057 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3058 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3059 }
3060 Self::Range(_, _) => self,
3061 }
3062 }
3063
3064 pub(crate) fn allows_prereleases(&self) -> bool {
3066 match self {
3067 Self::Default => false,
3068 Self::Any => true,
3069 Self::Major(..) => false,
3070 Self::MajorMinor(..) => false,
3071 Self::MajorMinorPatch(..) => false,
3072 Self::MajorMinorPrerelease(..) => true,
3073 Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3074 }
3075 }
3076
3077 pub(crate) fn is_debug(&self) -> bool {
3079 match self {
3080 Self::Any | Self::Default => false,
3081 Self::Major(_, variant)
3082 | Self::MajorMinor(_, _, variant)
3083 | Self::MajorMinorPatch(_, _, _, variant)
3084 | Self::MajorMinorPrerelease(_, _, _, variant)
3085 | Self::Range(_, variant) => variant.is_debug(),
3086 }
3087 }
3088
3089 pub(crate) fn is_freethreaded(&self) -> bool {
3091 match self {
3092 Self::Any | Self::Default => false,
3093 Self::Major(_, variant)
3094 | Self::MajorMinor(_, _, variant)
3095 | Self::MajorMinorPatch(_, _, _, variant)
3096 | Self::MajorMinorPrerelease(_, _, _, variant)
3097 | Self::Range(_, variant) => variant.is_freethreaded(),
3098 }
3099 }
3100
3101 #[must_use]
3105 pub fn without_python_variant(self) -> Self {
3106 match self {
3109 Self::Any | Self::Default => self,
3110 Self::Major(major, _) => Self::Major(major, PythonVariant::Default),
3111 Self::MajorMinor(major, minor, _) => {
3112 Self::MajorMinor(major, minor, PythonVariant::Default)
3113 }
3114 Self::MajorMinorPatch(major, minor, patch, _) => {
3115 Self::MajorMinorPatch(major, minor, patch, PythonVariant::Default)
3116 }
3117 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3118 Self::MajorMinorPrerelease(major, minor, prerelease, PythonVariant::Default)
3119 }
3120 Self::Range(specifiers, _) => Self::Range(specifiers, PythonVariant::Default),
3121 }
3122 }
3123
3124 pub(crate) fn variant(&self) -> Option<PythonVariant> {
3126 match self {
3127 Self::Any => None,
3128 Self::Default => Some(PythonVariant::Default),
3129 Self::Major(_, variant)
3130 | Self::MajorMinor(_, _, variant)
3131 | Self::MajorMinorPatch(_, _, _, variant)
3132 | Self::MajorMinorPrerelease(_, _, _, variant)
3133 | Self::Range(_, variant) => Some(*variant),
3134 }
3135 }
3136
3137 pub fn as_pep440_version(&self) -> Option<Version> {
3141 match self {
3142 Self::Default | Self::Any | Self::Range(_, _) => None,
3143 Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3144 Self::MajorMinor(major, minor, _) => {
3145 Some(Version::new([u64::from(*major), u64::from(*minor)]))
3146 }
3147 Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3148 u64::from(*major),
3149 u64::from(*minor),
3150 u64::from(*patch),
3151 ])),
3152 Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3154 Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3155 ),
3156 }
3157 }
3158
3159 pub fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
3165 match self {
3166 Self::Default | Self::Any => None,
3167 Self::Major(major, _) => Some(VersionSpecifiers::from(
3168 VersionSpecifier::equals_star_version(Version::new([u64::from(*major)])),
3169 )),
3170 Self::MajorMinor(major, minor, _) => Some(VersionSpecifiers::from(
3171 VersionSpecifier::equals_star_version(Version::new([
3172 u64::from(*major),
3173 u64::from(*minor),
3174 ])),
3175 )),
3176 Self::MajorMinorPatch(major, minor, patch, _) => {
3177 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3178 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)]),
3179 )))
3180 }
3181 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3182 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3183 Version::new([u64::from(*major), u64::from(*minor), 0])
3184 .with_pre(Some(*prerelease)),
3185 )))
3186 }
3187 Self::Range(specifiers, _) => Some(specifiers.clone()),
3188 }
3189 }
3190}
3191
3192impl FromStr for VersionRequest {
3193 type Err = Error;
3194
3195 fn from_str(s: &str) -> Result<Self, Self::Err> {
3196 fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3199 if s.chars().all(char::is_alphabetic) {
3201 return Err(Error::InvalidVersionRequest(s.to_string()));
3202 }
3203
3204 let Some(mut start) = s.rfind(|c: char| c.is_numeric()) else {
3205 return Ok((s, PythonVariant::Default));
3206 };
3207
3208 start += 1;
3210
3211 if start + 1 > s.len() {
3213 return Ok((s, PythonVariant::Default));
3214 }
3215
3216 let variant = &s[start..];
3217 let prefix = &s[..start];
3218
3219 let variant = variant.strip_prefix('+').unwrap_or(variant);
3221
3222 let Ok(variant) = PythonVariant::from_str(variant) else {
3226 return Ok((s, PythonVariant::Default));
3227 };
3228
3229 Ok((prefix, variant))
3230 }
3231
3232 let (s, variant) = parse_variant(s)?;
3233 let Ok(version) = Version::from_str(s) else {
3234 return parse_version_specifiers_request(s, variant);
3235 };
3236
3237 let version = split_wheel_tag_release_version(version);
3239
3240 if version.post().is_some() || version.dev().is_some() {
3242 return Err(Error::InvalidVersionRequest(s.to_string()));
3243 }
3244
3245 if !version.local().is_empty() {
3248 return Err(Error::InvalidVersionRequest(s.to_string()));
3249 }
3250
3251 let Ok(release) = try_into_u8_slice(&version.release()) else {
3253 return Err(Error::InvalidVersionRequest(s.to_string()));
3254 };
3255
3256 let prerelease = version.pre();
3257
3258 match release.as_slice() {
3259 [major] => {
3261 if prerelease.is_some() {
3263 return Err(Error::InvalidVersionRequest(s.to_string()));
3264 }
3265 Ok(Self::Major(*major, variant))
3266 }
3267 [major, minor] => {
3269 if let Some(prerelease) = prerelease {
3270 return Ok(Self::MajorMinorPrerelease(
3271 *major, *minor, prerelease, variant,
3272 ));
3273 }
3274 Ok(Self::MajorMinor(*major, *minor, variant))
3275 }
3276 [major, minor, patch] => {
3278 if let Some(prerelease) = prerelease {
3279 if *patch != 0 {
3282 return Err(Error::InvalidVersionRequest(s.to_string()));
3283 }
3284 return Ok(Self::MajorMinorPrerelease(
3285 *major, *minor, prerelease, variant,
3286 ));
3287 }
3288 Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3289 }
3290 _ => Err(Error::InvalidVersionRequest(s.to_string())),
3291 }
3292 }
3293}
3294
3295impl FromStr for PythonVariant {
3296 type Err = ();
3297
3298 fn from_str(s: &str) -> Result<Self, Self::Err> {
3299 match s {
3300 "t" | "freethreaded" => Ok(Self::Freethreaded),
3301 "d" | "debug" => Ok(Self::Debug),
3302 "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3303 "gil" => Ok(Self::Gil),
3304 "gil+debug" => Ok(Self::GilDebug),
3305 "" => Ok(Self::Default),
3306 _ => Err(()),
3307 }
3308 }
3309}
3310
3311impl fmt::Display for PythonVariant {
3312 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3313 match self {
3314 Self::Default => f.write_str("default"),
3315 Self::Debug => f.write_str("debug"),
3316 Self::Freethreaded => f.write_str("freethreaded"),
3317 Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3318 Self::Gil => f.write_str("gil"),
3319 Self::GilDebug => f.write_str("gil+debug"),
3320 }
3321 }
3322}
3323
3324fn parse_version_specifiers_request(
3325 s: &str,
3326 variant: PythonVariant,
3327) -> Result<VersionRequest, Error> {
3328 let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3329 return Err(Error::InvalidVersionRequest(s.to_string()));
3330 };
3331 if specifiers.is_empty() {
3332 return Err(Error::InvalidVersionRequest(s.to_string()));
3333 }
3334 Ok(VersionRequest::Range(specifiers, variant))
3335}
3336
3337impl From<&PythonVersion> for VersionRequest {
3338 fn from(version: &PythonVersion) -> Self {
3339 Self::from_str(&version.string)
3340 .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3341 }
3342}
3343
3344impl fmt::Display for VersionRequest {
3345 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3346 match self {
3347 Self::Any => f.write_str("any"),
3348 Self::Default => f.write_str("default"),
3349 Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3350 Self::MajorMinor(major, minor, variant) => {
3351 write!(f, "{major}.{minor}{}", variant.display_suffix())
3352 }
3353 Self::MajorMinorPatch(major, minor, patch, variant) => {
3354 write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3355 }
3356 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3357 write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3358 }
3359 Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3360 }
3361 }
3362}
3363
3364impl fmt::Display for PythonRequest {
3365 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3366 match self {
3367 Self::Default => write!(f, "a default Python"),
3368 Self::Any => write!(f, "any Python"),
3369 Self::Version(version) => write!(f, "Python {version}"),
3370 Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3371 Self::File(path) => write!(f, "path `{}`", path.user_display()),
3372 Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3373 Self::Implementation(implementation) => {
3374 write!(f, "{}", implementation.pretty())
3375 }
3376 Self::ImplementationVersion(implementation, version) => {
3377 write!(f, "{} {version}", implementation.pretty())
3378 }
3379 Self::Key(request) => write!(f, "{request}"),
3380 }
3381 }
3382}
3383
3384impl fmt::Display for PythonSource {
3385 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3386 match self {
3387 Self::ProvidedPath => f.write_str("provided path"),
3388 Self::ActiveEnvironment => f.write_str("active virtual environment"),
3389 Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3390 Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3391 Self::SearchPath => f.write_str("search path"),
3392 Self::SearchPathFirst => f.write_str("first executable in the search path"),
3393 Self::Registry => f.write_str("registry"),
3394 Self::MicrosoftStore => f.write_str("Microsoft Store"),
3395 Self::Managed => f.write_str("managed installations"),
3396 Self::ParentInterpreter => f.write_str("parent interpreter"),
3397 }
3398 }
3399}
3400
3401impl PythonPreference {
3402 fn sources(self) -> &'static [PythonSource] {
3405 match self {
3406 Self::OnlyManaged => &[PythonSource::Managed],
3407 Self::Managed => {
3408 if cfg!(windows) {
3409 &[
3410 PythonSource::Managed,
3411 PythonSource::SearchPath,
3412 PythonSource::Registry,
3413 ]
3414 } else {
3415 &[PythonSource::Managed, PythonSource::SearchPath]
3416 }
3417 }
3418 Self::System => {
3419 if cfg!(windows) {
3420 &[
3421 PythonSource::SearchPath,
3422 PythonSource::Registry,
3423 PythonSource::Managed,
3424 ]
3425 } else {
3426 &[PythonSource::SearchPath, PythonSource::Managed]
3427 }
3428 }
3429 Self::OnlySystem => {
3430 if cfg!(windows) {
3431 &[PythonSource::SearchPath, PythonSource::Registry]
3432 } else {
3433 &[PythonSource::SearchPath]
3434 }
3435 }
3436 }
3437 }
3438
3439 pub fn canonical_name(&self) -> &'static str {
3443 match self {
3444 Self::OnlyManaged => "only managed",
3445 Self::Managed => "prefer managed",
3446 Self::System => "prefer system",
3447 Self::OnlySystem => "only system",
3448 }
3449 }
3450}
3451
3452impl fmt::Display for PythonPreference {
3453 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3454 f.write_str(match self {
3455 Self::OnlyManaged => "only managed",
3456 Self::Managed => "prefer managed",
3457 Self::System => "prefer system",
3458 Self::OnlySystem => "only system",
3459 })
3460 }
3461}
3462
3463impl DiscoveryPreferences {
3464 fn sources(&self, request: &PythonRequest) -> String {
3467 let python_sources = self
3468 .python_preference
3469 .sources()
3470 .iter()
3471 .map(ToString::to_string)
3472 .collect::<Vec<_>>();
3473 match self.environment_preference {
3474 EnvironmentPreference::Any => disjunction(
3475 &["virtual environments"]
3476 .into_iter()
3477 .chain(python_sources.iter().map(String::as_str))
3478 .collect::<Vec<_>>(),
3479 ),
3480 EnvironmentPreference::ExplicitSystem => {
3481 if request.is_explicit_system() {
3482 disjunction(
3483 &["virtual environments"]
3484 .into_iter()
3485 .chain(python_sources.iter().map(String::as_str))
3486 .collect::<Vec<_>>(),
3487 )
3488 } else {
3489 disjunction(&["virtual environments"])
3490 }
3491 }
3492 EnvironmentPreference::OnlySystem => disjunction(
3493 &python_sources
3494 .iter()
3495 .map(String::as_str)
3496 .collect::<Vec<_>>(),
3497 ),
3498 EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3499 }
3500 }
3501}
3502
3503impl fmt::Display for PythonNotFound {
3504 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3505 let sources = DiscoveryPreferences {
3506 python_preference: self.python_preference,
3507 environment_preference: self.environment_preference,
3508 }
3509 .sources(&self.request);
3510
3511 match self.request {
3512 PythonRequest::Default | PythonRequest::Any => {
3513 write!(f, "No interpreter found in {sources}")
3514 }
3515 PythonRequest::File(_) => {
3516 write!(f, "No interpreter found at {}", self.request)
3517 }
3518 PythonRequest::Directory(_) => {
3519 write!(f, "No interpreter found in {}", self.request)
3520 }
3521 _ => {
3522 write!(f, "No interpreter found for {} in {sources}", self.request)
3523 }
3524 }
3525 }
3526}
3527
3528fn disjunction(items: &[&str]) -> String {
3530 match items.len() {
3531 0 => String::new(),
3532 1 => items[0].to_string(),
3533 2 => format!("{} or {}", items[0], items[1]),
3534 _ => {
3535 let last = items.last().unwrap();
3536 format!(
3537 "{}, or {}",
3538 items.iter().take(items.len() - 1).join(", "),
3539 last
3540 )
3541 }
3542 }
3543}
3544
3545fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3546 release
3547 .iter()
3548 .map(|x| match u8::try_from(*x) {
3549 Ok(x) => Ok(x),
3550 Err(e) => Err(e),
3551 })
3552 .collect()
3553}
3554
3555fn split_wheel_tag_release_version(version: Version) -> Version {
3562 let release = version.release();
3563 if release.len() != 1 {
3564 return version;
3565 }
3566
3567 let release = release[0].to_string();
3568 let mut chars = release.chars();
3569 let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3570 return version;
3571 };
3572
3573 let Ok(minor) = chars.as_str().parse::<u32>() else {
3574 return version;
3575 };
3576
3577 version.with_release([u64::from(major), u64::from(minor)])
3578}
3579
3580#[cfg(test)]
3581mod tests {
3582 use std::{path::PathBuf, str::FromStr};
3583
3584 use assert_fs::{TempDir, prelude::*};
3585 use target_lexicon::{Aarch64Architecture, Architecture};
3586 use test_log::test;
3587 use uv_distribution_types::RequiresPython;
3588 use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3589
3590 use crate::{
3591 discovery::{PythonRequest, VersionRequest},
3592 downloads::{ArchRequest, PythonDownloadRequest},
3593 implementation::ImplementationName,
3594 };
3595 use uv_platform::{Arch, Libc, Os};
3596
3597 use super::{
3598 DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonVariant,
3599 };
3600
3601 #[test]
3602 fn interpreter_request_from_str() {
3603 assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3604 assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
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"),
3611 PythonRequest::Version(VersionRequest::from_str(">=3.12").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 assert_eq!(
3618 PythonRequest::parse(">=3.12,<3.13"),
3619 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3620 );
3621
3622 assert_eq!(
3623 PythonRequest::parse("3.13.0a1"),
3624 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3625 );
3626 assert_eq!(
3627 PythonRequest::parse("3.13.0b5"),
3628 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3629 );
3630 assert_eq!(
3631 PythonRequest::parse("3.13.0rc1"),
3632 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3633 );
3634 assert_eq!(
3635 PythonRequest::parse("3.13.1rc1"),
3636 PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3637 "Pre-release version requests require a patch version of zero"
3638 );
3639 assert_eq!(
3640 PythonRequest::parse("3rc1"),
3641 PythonRequest::ExecutableName("3rc1".to_string()),
3642 "Pre-release version requests require a minor version"
3643 );
3644
3645 assert_eq!(
3646 PythonRequest::parse("cpython"),
3647 PythonRequest::Implementation(ImplementationName::CPython)
3648 );
3649
3650 assert_eq!(
3651 PythonRequest::parse("cpython3.12.2"),
3652 PythonRequest::ImplementationVersion(
3653 ImplementationName::CPython,
3654 VersionRequest::from_str("3.12.2").unwrap(),
3655 )
3656 );
3657
3658 assert_eq!(
3659 PythonRequest::parse("cpython-3.13.2"),
3660 PythonRequest::Key(PythonDownloadRequest {
3661 version: Some(VersionRequest::MajorMinorPatch(
3662 3,
3663 13,
3664 2,
3665 PythonVariant::Default
3666 )),
3667 implementation: Some(ImplementationName::CPython),
3668 arch: None,
3669 os: None,
3670 libc: None,
3671 build: None,
3672 prereleases: None
3673 })
3674 );
3675 assert_eq!(
3676 PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3677 PythonRequest::Key(PythonDownloadRequest {
3678 version: Some(VersionRequest::MajorMinorPatch(
3679 3,
3680 13,
3681 2,
3682 PythonVariant::Default
3683 )),
3684 implementation: Some(ImplementationName::CPython),
3685 arch: Some(ArchRequest::Explicit(Arch::new(
3686 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3687 None
3688 ))),
3689 os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3690 libc: Some(Libc::None),
3691 build: None,
3692 prereleases: None
3693 })
3694 );
3695 assert_eq!(
3696 PythonRequest::parse("any-3.13.2"),
3697 PythonRequest::Key(PythonDownloadRequest {
3698 version: Some(VersionRequest::MajorMinorPatch(
3699 3,
3700 13,
3701 2,
3702 PythonVariant::Default
3703 )),
3704 implementation: None,
3705 arch: None,
3706 os: None,
3707 libc: None,
3708 build: None,
3709 prereleases: None
3710 })
3711 );
3712 assert_eq!(
3713 PythonRequest::parse("any-3.13.2-any-aarch64"),
3714 PythonRequest::Key(PythonDownloadRequest {
3715 version: Some(VersionRequest::MajorMinorPatch(
3716 3,
3717 13,
3718 2,
3719 PythonVariant::Default
3720 )),
3721 implementation: None,
3722 arch: Some(ArchRequest::Explicit(Arch::new(
3723 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3724 None
3725 ))),
3726 os: None,
3727 libc: None,
3728 build: None,
3729 prereleases: None
3730 })
3731 );
3732
3733 assert_eq!(
3734 PythonRequest::parse("pypy"),
3735 PythonRequest::Implementation(ImplementationName::PyPy)
3736 );
3737 assert_eq!(
3738 PythonRequest::parse("pp"),
3739 PythonRequest::Implementation(ImplementationName::PyPy)
3740 );
3741 assert_eq!(
3742 PythonRequest::parse("graalpy"),
3743 PythonRequest::Implementation(ImplementationName::GraalPy)
3744 );
3745 assert_eq!(
3746 PythonRequest::parse("gp"),
3747 PythonRequest::Implementation(ImplementationName::GraalPy)
3748 );
3749 assert_eq!(
3750 PythonRequest::parse("cp"),
3751 PythonRequest::Implementation(ImplementationName::CPython)
3752 );
3753 assert_eq!(
3754 PythonRequest::parse("pypy3.10"),
3755 PythonRequest::ImplementationVersion(
3756 ImplementationName::PyPy,
3757 VersionRequest::from_str("3.10").unwrap(),
3758 )
3759 );
3760 assert_eq!(
3761 PythonRequest::parse("pp310"),
3762 PythonRequest::ImplementationVersion(
3763 ImplementationName::PyPy,
3764 VersionRequest::from_str("3.10").unwrap(),
3765 )
3766 );
3767 assert_eq!(
3768 PythonRequest::parse("graalpy3.10"),
3769 PythonRequest::ImplementationVersion(
3770 ImplementationName::GraalPy,
3771 VersionRequest::from_str("3.10").unwrap(),
3772 )
3773 );
3774 assert_eq!(
3775 PythonRequest::parse("gp310"),
3776 PythonRequest::ImplementationVersion(
3777 ImplementationName::GraalPy,
3778 VersionRequest::from_str("3.10").unwrap(),
3779 )
3780 );
3781 assert_eq!(
3782 PythonRequest::parse("cp38"),
3783 PythonRequest::ImplementationVersion(
3784 ImplementationName::CPython,
3785 VersionRequest::from_str("3.8").unwrap(),
3786 )
3787 );
3788 assert_eq!(
3789 PythonRequest::parse("pypy@3.10"),
3790 PythonRequest::ImplementationVersion(
3791 ImplementationName::PyPy,
3792 VersionRequest::from_str("3.10").unwrap(),
3793 )
3794 );
3795 assert_eq!(
3796 PythonRequest::parse("pypy310"),
3797 PythonRequest::ImplementationVersion(
3798 ImplementationName::PyPy,
3799 VersionRequest::from_str("3.10").unwrap(),
3800 )
3801 );
3802 assert_eq!(
3803 PythonRequest::parse("graalpy@3.10"),
3804 PythonRequest::ImplementationVersion(
3805 ImplementationName::GraalPy,
3806 VersionRequest::from_str("3.10").unwrap(),
3807 )
3808 );
3809 assert_eq!(
3810 PythonRequest::parse("graalpy310"),
3811 PythonRequest::ImplementationVersion(
3812 ImplementationName::GraalPy,
3813 VersionRequest::from_str("3.10").unwrap(),
3814 )
3815 );
3816
3817 let tempdir = TempDir::new().unwrap();
3818 assert_eq!(
3819 PythonRequest::parse(tempdir.path().to_str().unwrap()),
3820 PythonRequest::Directory(tempdir.path().to_path_buf()),
3821 "An existing directory is treated as a directory"
3822 );
3823 assert_eq!(
3824 PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3825 PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3826 "A path that does not exist is treated as a file"
3827 );
3828 tempdir.child("bar").touch().unwrap();
3829 assert_eq!(
3830 PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3831 PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3832 "An existing file is treated as a file"
3833 );
3834 assert_eq!(
3835 PythonRequest::parse("./foo"),
3836 PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
3837 "A string with a file system separator is treated as a file"
3838 );
3839 assert_eq!(
3840 PythonRequest::parse("3.13t"),
3841 PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
3842 );
3843 }
3844
3845 #[test]
3846 fn discovery_sources_prefer_system_orders_search_path_first() {
3847 let preferences = DiscoveryPreferences {
3848 python_preference: PythonPreference::System,
3849 environment_preference: EnvironmentPreference::OnlySystem,
3850 };
3851 let sources = preferences.sources(&PythonRequest::Default);
3852
3853 if cfg!(windows) {
3854 assert_eq!(sources, "search path, registry, or managed installations");
3855 } else {
3856 assert_eq!(sources, "search path or managed installations");
3857 }
3858 }
3859
3860 #[test]
3861 fn discovery_sources_only_system_matches_platform_order() {
3862 let preferences = DiscoveryPreferences {
3863 python_preference: PythonPreference::OnlySystem,
3864 environment_preference: EnvironmentPreference::OnlySystem,
3865 };
3866 let sources = preferences.sources(&PythonRequest::Default);
3867
3868 if cfg!(windows) {
3869 assert_eq!(sources, "search path or registry");
3870 } else {
3871 assert_eq!(sources, "search path");
3872 }
3873 }
3874
3875 #[test]
3876 fn interpreter_request_to_canonical_string() {
3877 assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
3878 assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
3879 assert_eq!(
3880 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
3881 "3.12"
3882 );
3883 assert_eq!(
3884 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3885 .to_canonical_string(),
3886 ">=3.12"
3887 );
3888 assert_eq!(
3889 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3890 .to_canonical_string(),
3891 ">=3.12, <3.13"
3892 );
3893
3894 assert_eq!(
3895 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3896 .to_canonical_string(),
3897 "3.13a1"
3898 );
3899
3900 assert_eq!(
3901 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3902 .to_canonical_string(),
3903 "3.13b5"
3904 );
3905
3906 assert_eq!(
3907 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3908 .to_canonical_string(),
3909 "3.13rc1"
3910 );
3911
3912 assert_eq!(
3913 PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
3914 .to_canonical_string(),
3915 "3.13rc4"
3916 );
3917
3918 assert_eq!(
3919 PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
3920 "foo"
3921 );
3922 assert_eq!(
3923 PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
3924 "cpython"
3925 );
3926 assert_eq!(
3927 PythonRequest::ImplementationVersion(
3928 ImplementationName::CPython,
3929 VersionRequest::from_str("3.12.2").unwrap(),
3930 )
3931 .to_canonical_string(),
3932 "cpython@3.12.2"
3933 );
3934 assert_eq!(
3935 PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
3936 "pypy"
3937 );
3938 assert_eq!(
3939 PythonRequest::ImplementationVersion(
3940 ImplementationName::PyPy,
3941 VersionRequest::from_str("3.10").unwrap(),
3942 )
3943 .to_canonical_string(),
3944 "pypy@3.10"
3945 );
3946 assert_eq!(
3947 PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
3948 "graalpy"
3949 );
3950 assert_eq!(
3951 PythonRequest::ImplementationVersion(
3952 ImplementationName::GraalPy,
3953 VersionRequest::from_str("3.10").unwrap(),
3954 )
3955 .to_canonical_string(),
3956 "graalpy@3.10"
3957 );
3958
3959 let tempdir = TempDir::new().unwrap();
3960 assert_eq!(
3961 PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
3962 tempdir.path().to_str().unwrap(),
3963 "An existing directory is treated as a directory"
3964 );
3965 assert_eq!(
3966 PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
3967 tempdir.child("foo").path().to_str().unwrap(),
3968 "A path that does not exist is treated as a file"
3969 );
3970 tempdir.child("bar").touch().unwrap();
3971 assert_eq!(
3972 PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
3973 tempdir.child("bar").path().to_str().unwrap(),
3974 "An existing file is treated as a file"
3975 );
3976 assert_eq!(
3977 PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
3978 "./foo",
3979 "A string with a file system separator is treated as a file"
3980 );
3981 }
3982
3983 #[test]
3984 fn version_request_from_str() {
3985 assert_eq!(
3986 VersionRequest::from_str("3").unwrap(),
3987 VersionRequest::Major(3, PythonVariant::Default)
3988 );
3989 assert_eq!(
3990 VersionRequest::from_str("3.12").unwrap(),
3991 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
3992 );
3993 assert_eq!(
3994 VersionRequest::from_str("3.12.1").unwrap(),
3995 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
3996 );
3997 assert!(VersionRequest::from_str("1.foo.1").is_err());
3998 assert_eq!(
3999 VersionRequest::from_str("3").unwrap(),
4000 VersionRequest::Major(3, PythonVariant::Default)
4001 );
4002 assert_eq!(
4003 VersionRequest::from_str("38").unwrap(),
4004 VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
4005 );
4006 assert_eq!(
4007 VersionRequest::from_str("312").unwrap(),
4008 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4009 );
4010 assert_eq!(
4011 VersionRequest::from_str("3100").unwrap(),
4012 VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
4013 );
4014 assert_eq!(
4015 VersionRequest::from_str("3.13a1").unwrap(),
4016 VersionRequest::MajorMinorPrerelease(
4017 3,
4018 13,
4019 Prerelease {
4020 kind: PrereleaseKind::Alpha,
4021 number: 1
4022 },
4023 PythonVariant::Default
4024 )
4025 );
4026 assert_eq!(
4027 VersionRequest::from_str("313b1").unwrap(),
4028 VersionRequest::MajorMinorPrerelease(
4029 3,
4030 13,
4031 Prerelease {
4032 kind: PrereleaseKind::Beta,
4033 number: 1
4034 },
4035 PythonVariant::Default
4036 )
4037 );
4038 assert_eq!(
4039 VersionRequest::from_str("3.13.0b2").unwrap(),
4040 VersionRequest::MajorMinorPrerelease(
4041 3,
4042 13,
4043 Prerelease {
4044 kind: PrereleaseKind::Beta,
4045 number: 2
4046 },
4047 PythonVariant::Default
4048 )
4049 );
4050 assert_eq!(
4051 VersionRequest::from_str("3.13.0rc3").unwrap(),
4052 VersionRequest::MajorMinorPrerelease(
4053 3,
4054 13,
4055 Prerelease {
4056 kind: PrereleaseKind::Rc,
4057 number: 3
4058 },
4059 PythonVariant::Default
4060 )
4061 );
4062 assert!(
4063 matches!(
4064 VersionRequest::from_str("3rc1"),
4065 Err(Error::InvalidVersionRequest(_))
4066 ),
4067 "Pre-release version requests require a minor version"
4068 );
4069 assert!(
4070 matches!(
4071 VersionRequest::from_str("3.13.2rc1"),
4072 Err(Error::InvalidVersionRequest(_))
4073 ),
4074 "Pre-release version requests require a patch version of zero"
4075 );
4076 assert!(
4077 matches!(
4078 VersionRequest::from_str("3.12-dev"),
4079 Err(Error::InvalidVersionRequest(_))
4080 ),
4081 "Development version segments are not allowed"
4082 );
4083 assert!(
4084 matches!(
4085 VersionRequest::from_str("3.12+local"),
4086 Err(Error::InvalidVersionRequest(_))
4087 ),
4088 "Local version segments are not allowed"
4089 );
4090 assert!(
4091 matches!(
4092 VersionRequest::from_str("3.12.post0"),
4093 Err(Error::InvalidVersionRequest(_))
4094 ),
4095 "Post version segments are not allowed"
4096 );
4097 assert!(
4098 matches!(
4100 VersionRequest::from_str("31000"),
4101 Err(Error::InvalidVersionRequest(_))
4102 )
4103 );
4104 assert_eq!(
4105 VersionRequest::from_str("3t").unwrap(),
4106 VersionRequest::Major(3, PythonVariant::Freethreaded)
4107 );
4108 assert_eq!(
4109 VersionRequest::from_str("313t").unwrap(),
4110 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4111 );
4112 assert_eq!(
4113 VersionRequest::from_str("3.13t").unwrap(),
4114 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4115 );
4116 assert_eq!(
4117 VersionRequest::from_str(">=3.13t").unwrap(),
4118 VersionRequest::Range(
4119 VersionSpecifiers::from_str(">=3.13").unwrap(),
4120 PythonVariant::Freethreaded
4121 )
4122 );
4123 assert_eq!(
4124 VersionRequest::from_str(">=3.13").unwrap(),
4125 VersionRequest::Range(
4126 VersionSpecifiers::from_str(">=3.13").unwrap(),
4127 PythonVariant::Default
4128 )
4129 );
4130 assert_eq!(
4131 VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4132 VersionRequest::Range(
4133 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4134 PythonVariant::Freethreaded
4135 )
4136 );
4137 assert!(matches!(
4138 VersionRequest::from_str("3.13tt"),
4139 Err(Error::InvalidVersionRequest(_))
4140 ));
4141 }
4142
4143 #[test]
4144 fn executable_names_from_request() {
4145 fn case(request: &str, expected: &[&str]) {
4146 let (implementation, version) = match PythonRequest::parse(request) {
4147 PythonRequest::Any => (None, VersionRequest::Any),
4148 PythonRequest::Default => (None, VersionRequest::Default),
4149 PythonRequest::Version(version) => (None, version),
4150 PythonRequest::ImplementationVersion(implementation, version) => {
4151 (Some(implementation), version)
4152 }
4153 PythonRequest::Implementation(implementation) => {
4154 (Some(implementation), VersionRequest::Default)
4155 }
4156 result => {
4157 panic!("Test cases should request versions or implementations; got {result:?}")
4158 }
4159 };
4160
4161 let result: Vec<_> = version
4162 .executable_names(implementation.as_ref())
4163 .into_iter()
4164 .map(|name| name.to_string())
4165 .collect();
4166
4167 let expected: Vec<_> = expected
4168 .iter()
4169 .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4170 .collect();
4171
4172 assert_eq!(result, expected, "mismatch for case \"{request}\"");
4173 }
4174
4175 case(
4176 "any",
4177 &[
4178 "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4179 "pyodide", "pyodide3",
4180 ],
4181 );
4182
4183 case("default", &["python", "python3"]);
4184
4185 case("3", &["python3", "python"]);
4186
4187 case("4", &["python4", "python"]);
4188
4189 case("3.13", &["python3.13", "python3", "python"]);
4190
4191 case("pypy", &["pypy", "pypy3", "python", "python3"]);
4192
4193 case(
4194 "pypy@3.10",
4195 &[
4196 "pypy3.10",
4197 "pypy3",
4198 "pypy",
4199 "python3.10",
4200 "python3",
4201 "python",
4202 ],
4203 );
4204
4205 case(
4206 "3.13t",
4207 &[
4208 "python3.13t",
4209 "python3.13",
4210 "python3t",
4211 "python3",
4212 "pythont",
4213 "python",
4214 ],
4215 );
4216 case("3t", &["python3t", "python3", "pythont", "python"]);
4217
4218 case(
4219 "3.13.2",
4220 &["python3.13.2", "python3.13", "python3", "python"],
4221 );
4222
4223 case(
4224 "3.13rc2",
4225 &["python3.13rc2", "python3.13", "python3", "python"],
4226 );
4227 }
4228
4229 #[test]
4230 fn test_try_split_prefix_and_version() {
4231 assert!(matches!(
4232 PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4233 Ok(None),
4234 ));
4235 assert!(matches!(
4236 PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4237 Ok(Some(_)),
4238 ));
4239 assert!(matches!(
4240 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4241 Ok(Some(_)),
4242 ));
4243 assert!(matches!(
4244 PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4245 Ok(None),
4246 ));
4247 assert!(
4249 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4250 );
4251 assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4253 }
4254
4255 #[test]
4256 fn version_request_as_pep440_version() {
4257 assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4259 assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4260 assert_eq!(
4261 VersionRequest::from_str(">=3.10")
4262 .unwrap()
4263 .as_pep440_version(),
4264 None
4265 );
4266
4267 assert_eq!(
4269 VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4270 Some(Version::from_str("3").unwrap())
4271 );
4272
4273 assert_eq!(
4275 VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4276 Some(Version::from_str("3.12").unwrap())
4277 );
4278
4279 assert_eq!(
4281 VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4282 Some(Version::from_str("3.12.5").unwrap())
4283 );
4284
4285 assert_eq!(
4287 VersionRequest::MajorMinorPrerelease(
4288 3,
4289 14,
4290 Prerelease {
4291 kind: PrereleaseKind::Alpha,
4292 number: 1
4293 },
4294 PythonVariant::Default
4295 )
4296 .as_pep440_version(),
4297 Some(Version::from_str("3.14.0a1").unwrap())
4298 );
4299 assert_eq!(
4300 VersionRequest::MajorMinorPrerelease(
4301 3,
4302 14,
4303 Prerelease {
4304 kind: PrereleaseKind::Beta,
4305 number: 2
4306 },
4307 PythonVariant::Default
4308 )
4309 .as_pep440_version(),
4310 Some(Version::from_str("3.14.0b2").unwrap())
4311 );
4312 assert_eq!(
4313 VersionRequest::MajorMinorPrerelease(
4314 3,
4315 13,
4316 Prerelease {
4317 kind: PrereleaseKind::Rc,
4318 number: 3
4319 },
4320 PythonVariant::Default
4321 )
4322 .as_pep440_version(),
4323 Some(Version::from_str("3.13.0rc3").unwrap())
4324 );
4325
4326 assert_eq!(
4328 VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4329 Some(Version::from_str("3").unwrap())
4330 );
4331 assert_eq!(
4332 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4333 Some(Version::from_str("3.13").unwrap())
4334 );
4335 }
4336
4337 #[test]
4338 fn python_request_as_pep440_version() {
4339 assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4341 assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4342
4343 assert_eq!(
4345 PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4346 .as_pep440_version(),
4347 Some(Version::from_str("3.11").unwrap())
4348 );
4349
4350 assert_eq!(
4352 PythonRequest::ImplementationVersion(
4353 ImplementationName::CPython,
4354 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4355 )
4356 .as_pep440_version(),
4357 Some(Version::from_str("3.12.1").unwrap())
4358 );
4359
4360 assert_eq!(
4362 PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4363 None
4364 );
4365
4366 assert_eq!(
4368 PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4369 Some(Version::from_str("3.13.2").unwrap())
4370 );
4371
4372 assert_eq!(
4374 PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4375 None
4376 );
4377
4378 assert_eq!(
4380 PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4381 None
4382 );
4383 }
4384
4385 #[test]
4386 fn intersects_requires_python_exact() {
4387 let requires_python =
4388 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4389
4390 assert!(PythonRequest::parse("3.12").intersects_requires_python(&requires_python));
4391 assert!(!PythonRequest::parse("3.11").intersects_requires_python(&requires_python));
4392 }
4393
4394 #[test]
4395 fn intersects_requires_python_major() {
4396 let requires_python =
4397 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4398
4399 assert!(PythonRequest::parse("3").intersects_requires_python(&requires_python));
4401 assert!(!PythonRequest::parse("2").intersects_requires_python(&requires_python));
4403 }
4404
4405 #[test]
4406 fn intersects_requires_python_range() {
4407 let requires_python =
4408 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4409
4410 assert!(PythonRequest::parse(">=3.12,<3.13").intersects_requires_python(&requires_python));
4411 assert!(!PythonRequest::parse(">=3.10,<3.12").intersects_requires_python(&requires_python));
4412 }
4413
4414 #[test]
4415 fn intersects_requires_python_implementation_range() {
4416 let requires_python =
4417 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4418
4419 assert!(
4420 PythonRequest::parse("cpython@>=3.12,<3.13")
4421 .intersects_requires_python(&requires_python)
4422 );
4423 assert!(
4424 !PythonRequest::parse("cpython@>=3.10,<3.12")
4425 .intersects_requires_python(&requires_python)
4426 );
4427 }
4428
4429 #[test]
4430 fn intersects_requires_python_no_version() {
4431 let requires_python =
4432 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4433
4434 assert!(PythonRequest::Any.intersects_requires_python(&requires_python));
4436 assert!(PythonRequest::Default.intersects_requires_python(&requires_python));
4437 assert!(
4438 PythonRequest::Implementation(ImplementationName::CPython)
4439 .intersects_requires_python(&requires_python)
4440 );
4441 }
4442}