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