1use std::borrow::Cow;
2use std::fmt;
3use std::hash::{Hash, Hasher};
4use std::str::FromStr;
5
6use indexmap::IndexMap;
7use ref_cast::RefCast;
8use reqwest_retry::policies::ExponentialBackoff;
9use tracing::{debug, info};
10use uv_warnings::warn_user;
11
12use uv_cache::Cache;
13use uv_client::{BaseClient, BaseClientBuilder};
14use uv_pep440::{Prerelease, Version};
15use uv_platform::{Arch, Libc, Os, Platform};
16
17use crate::discovery::{
18 EnvironmentPreference, PythonRequest, find_best_python_installation, find_python_installation,
19};
20use crate::downloads::{
21 DownloadResult, ManagedPythonDownload, ManagedPythonDownloadList, PythonDownloadRequest,
22 Reporter,
23};
24use crate::implementation::LenientImplementationName;
25use crate::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
26use crate::{
27 Error, ImplementationName, Interpreter, MissingPythonHint, PythonDownloads, PythonPreference,
28 PythonSource, PythonVariant, PythonVersion, downloads,
29};
30
31#[derive(Clone, Debug)]
33pub struct PythonInstallation {
34 pub(crate) source: PythonSource,
36 pub(crate) interpreter: Interpreter,
37}
38
39impl PythonInstallation {
40 pub fn new(source: PythonSource, interpreter: Interpreter) -> Self {
42 Self {
43 source,
44 interpreter,
45 }
46 }
47
48 pub fn find(
61 request: &PythonRequest,
62 environments: EnvironmentPreference,
63 preference: PythonPreference,
64 download_list: &ManagedPythonDownloadList,
65 cache: &Cache,
66 ) -> Result<Self, Error> {
67 let installation = Self::find_existing(request, environments, preference, cache)?;
68 installation.warn_if_outdated_prerelease(request, download_list);
69 Ok(installation)
70 }
71
72 pub fn find_existing(
74 request: &PythonRequest,
75 environments: EnvironmentPreference,
76 preference: PythonPreference,
77 cache: &Cache,
78 ) -> Result<Self, Error> {
79 Ok(find_python_installation(
80 request,
81 environments,
82 preference,
83 cache,
84 )??)
85 }
86
87 pub async fn find_best(
90 request: &PythonRequest,
91 environments: EnvironmentPreference,
92 preference: PythonPreference,
93 python_downloads: PythonDownloads,
94 client_builder: &BaseClientBuilder<'_>,
95 cache: &Cache,
96 reporter: Option<&dyn Reporter>,
97 python_install_mirror: Option<&str>,
98 pypy_install_mirror: Option<&str>,
99 python_downloads_json_url: Option<&str>,
100 ) -> Result<Self, Error> {
101 let downloads_enabled = preference.allows_managed()
102 && python_downloads.is_automatic()
103 && client_builder.connectivity.is_online();
104 let installation = find_best_python_installation(
105 request,
106 environments,
107 preference,
108 downloads_enabled,
109 client_builder,
110 cache,
111 reporter,
112 python_install_mirror,
113 pypy_install_mirror,
114 python_downloads_json_url,
115 )
116 .await?;
117 installation
118 .download_and_warn_if_outdated_prerelease(
119 request,
120 client_builder,
121 python_downloads_json_url,
122 )
123 .await?;
124 Ok(installation)
125 }
126
127 pub async fn find_or_download(
131 request: Option<&PythonRequest>,
132 environments: EnvironmentPreference,
133 preference: PythonPreference,
134 python_downloads: PythonDownloads,
135 client_builder: &BaseClientBuilder<'_>,
136 cache: &Cache,
137 reporter: Option<&dyn Reporter>,
138 python_install_mirror: Option<&str>,
139 pypy_install_mirror: Option<&str>,
140 python_downloads_json_url: Option<&str>,
141 ) -> Result<Self, Error> {
142 let request = request.unwrap_or(&PythonRequest::Default);
143
144 let err = match Self::find_existing(request, environments, preference, cache) {
145 Ok(installation) => {
146 installation
147 .download_and_warn_if_outdated_prerelease(
148 request,
149 client_builder,
150 python_downloads_json_url,
151 )
152 .await?;
153 return Ok(installation);
154 }
155 Err(err) => err,
156 };
157
158 match err {
159 Error::MissingPython(..) => {}
161 Error::Discovery(ref err) if !err.is_critical() => {}
163 _ => return Err(err),
165 }
166
167 let Some(download_request) = PythonDownloadRequest::from_request(request) else {
169 return Err(err);
170 };
171
172 let download_list_client = client_builder.build()?;
173 let download_list =
174 ManagedPythonDownloadList::new(&download_list_client, python_downloads_json_url)
175 .await?;
176
177 let downloads_enabled = preference.allows_managed()
178 && python_downloads.is_automatic()
179 && client_builder.connectivity.is_online();
180
181 let download = download_request
182 .clone()
183 .fill()
184 .map(|request| download_list.find(&request));
185
186 let download = match download {
190 Ok(Ok(download)) => Some(download),
191 Ok(Err(downloads::Error::NoDownloadFound(_))) => {
193 if downloads_enabled {
194 debug!("No downloads are available for {request}");
195 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
196 return Err(err);
197 }
198 return Err(err.with_hint(MissingPythonHint::RequiresUpdate));
199 }
200 None
201 }
202 Err(err) | Ok(Err(err)) => {
203 if downloads_enabled {
204 return Err(err.into());
206 }
207 None
208 }
209 };
210
211 let Some(download) = download else {
212 debug_assert!(!downloads_enabled);
215 return Err(err);
216 };
217
218 if !downloads_enabled {
220 match python_downloads {
221 PythonDownloads::Automatic => {}
222 PythonDownloads::Manual => {
223 return Err(err.with_hint(MissingPythonHint::DownloadsManual(request.clone())));
224 }
225 PythonDownloads::Never => {
226 return Err(err.with_hint(MissingPythonHint::DownloadsNever(request.clone())));
227 }
228 }
229
230 match preference {
231 PythonPreference::OnlySystem => {
232 return Err(
233 err.with_hint(MissingPythonHint::PreferenceOnlySystem(request.clone()))
234 );
235 }
236 PythonPreference::Managed
237 | PythonPreference::OnlyManaged
238 | PythonPreference::System => {}
239 }
240
241 if !client_builder.connectivity.is_online() {
242 return Err(err.with_hint(MissingPythonHint::Offline(request.clone())));
243 }
244
245 return Err(err);
246 }
247
248 let retry_policy = client_builder.retry_policy();
251 let download_client = client_builder.clone().retries(0).build()?;
252
253 let installation = Self::fetch(
254 download,
255 &download_client,
256 &retry_policy,
257 cache,
258 reporter,
259 python_install_mirror,
260 pypy_install_mirror,
261 )
262 .await?;
263
264 installation.warn_if_outdated_prerelease(request, &download_list);
265
266 Ok(installation)
267 }
268
269 pub(crate) async fn fetch(
271 download: &ManagedPythonDownload,
272 client: &BaseClient,
273 retry_policy: &ExponentialBackoff,
274 cache: &Cache,
275 reporter: Option<&dyn Reporter>,
276 python_install_mirror: Option<&str>,
277 pypy_install_mirror: Option<&str>,
278 ) -> Result<Self, Error> {
279 let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
280 let installations_dir = installations.root();
281 let scratch_dir = installations.scratch();
282 let _lock = installations.lock().await?;
283
284 info!("Fetching requested Python...");
285 let result = download
286 .fetch_with_retry(
287 client,
288 retry_policy,
289 installations_dir,
290 &scratch_dir,
291 false,
292 python_install_mirror,
293 pypy_install_mirror,
294 reporter,
295 )
296 .await?;
297
298 let path = match result {
299 DownloadResult::AlreadyAvailable(path) => path,
300 DownloadResult::Fetched(path) => path,
301 };
302
303 let installed = ManagedPythonInstallation::new(path, download);
304 installed.ensure_externally_managed()?;
305 installed.ensure_sysconfig_patched()?;
306 installed.ensure_canonical_executables()?;
307 installed.ensure_build_file()?;
308
309 let minor_version = installed.minor_version_key();
310 let highest_patch = installations
311 .find_all()?
312 .filter(|installation| installation.minor_version_key() == minor_version)
313 .filter_map(|installation| installation.version().patch())
314 .fold(0, std::cmp::max);
315 if installed
316 .version()
317 .patch()
318 .is_some_and(|p| p >= highest_patch)
319 {
320 installed.ensure_minor_version_link()?;
321 }
322
323 if let Err(e) = installed.ensure_dylib_patched() {
324 e.warn_user(&installed);
325 }
326
327 Ok(Self {
328 source: PythonSource::Managed,
329 interpreter: Interpreter::query(installed.executable(false), cache)?,
330 })
331 }
332
333 pub fn source(&self) -> &PythonSource {
335 &self.source
336 }
337
338 pub fn key(&self) -> PythonInstallationKey {
339 self.interpreter.key()
340 }
341
342 pub fn python_version(&self) -> &Version {
344 self.interpreter.python_version()
345 }
346
347 pub fn implementation(&self) -> LenientImplementationName {
349 LenientImplementationName::from(self.interpreter.implementation_name())
350 }
351
352 pub(crate) fn is_managed(&self) -> bool {
356 self.source.is_managed() || self.interpreter.is_managed()
357 }
358
359 pub(crate) fn is_alternative_implementation(&self) -> bool {
363 !matches!(
364 self.implementation(),
365 LenientImplementationName::Known(ImplementationName::CPython)
366 ) || self.os().is_emscripten()
367 }
368
369 pub fn arch(&self) -> Arch {
371 self.interpreter.arch()
372 }
373
374 pub fn libc(&self) -> Libc {
376 self.interpreter.libc()
377 }
378
379 pub fn os(&self) -> Os {
381 self.interpreter.os()
382 }
383
384 pub fn interpreter(&self) -> &Interpreter {
386 &self.interpreter
387 }
388
389 pub fn into_interpreter(self) -> Interpreter {
391 self.interpreter
392 }
393
394 fn should_check_outdated_prerelease_warning(&self, request: &PythonRequest) -> bool {
396 if request.allows_prereleases() {
397 return false;
398 }
399
400 let interpreter = self.interpreter();
401
402 if interpreter.python_version().pre().is_none() {
403 return false;
404 }
405
406 if !interpreter.is_managed() {
407 return false;
408 }
409
410 if !interpreter
415 .implementation_name()
416 .eq_ignore_ascii_case("cpython")
417 {
418 return false;
419 }
420
421 true
422 }
423
424 fn warn_if_outdated_prerelease(
427 &self,
428 request: &PythonRequest,
429 download_list: &ManagedPythonDownloadList,
430 ) {
431 if !self.should_check_outdated_prerelease_warning(request) {
432 return;
433 }
434
435 let interpreter = self.interpreter();
436 let version = interpreter.python_version();
437
438 let release = version.only_release();
439
440 let Ok(download_request) = PythonDownloadRequest::try_from(&interpreter.key()) else {
441 return;
442 };
443
444 let download_request = download_request.with_prereleases(false);
445
446 let has_stable_download = {
447 let mut downloads = download_list.iter_matching(&download_request);
448
449 downloads.any(|download| {
450 let download_version = download.key().version().into_version();
451 download_version.pre().is_none() && download_version.only_release() >= release
452 })
453 };
454
455 if !has_stable_download {
456 return;
457 }
458
459 if let Some(upgrade_request) = download_request
460 .unset_defaults()
461 .without_patch()
462 .simplified_display()
463 {
464 warn_user!(
465 "You're using a pre-release version of Python ({}) but a stable version is available. Use `uv python upgrade {}` to upgrade.",
466 version,
467 upgrade_request
468 );
469 } else {
470 warn_user!(
471 "You're using a pre-release version of Python ({}) but a stable version is available. Run `uv python upgrade` to update your managed interpreters.",
472 version,
473 );
474 }
475 }
476
477 pub async fn download_and_warn_if_outdated_prerelease(
483 &self,
484 request: &PythonRequest,
485 client_builder: &BaseClientBuilder<'_>,
486 python_downloads_json_url: Option<&str>,
487 ) -> Result<(), Error> {
488 if !self.should_check_outdated_prerelease_warning(request) {
489 return Ok(());
490 }
491
492 let download_list_client = client_builder.build()?;
493 let download_list =
494 ManagedPythonDownloadList::new(&download_list_client, python_downloads_json_url)
495 .await?;
496 self.warn_if_outdated_prerelease(request, &download_list);
497
498 Ok(())
499 }
500}
501
502#[derive(Error, Debug)]
503pub enum PythonInstallationKeyError {
504 #[error("Failed to parse Python installation key `{0}`: {1}")]
505 ParseError(String, String),
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Hash)]
509pub struct PythonInstallationKey {
510 pub(crate) implementation: LenientImplementationName,
511 pub(crate) major: u8,
512 pub(crate) minor: u8,
513 pub(crate) patch: u8,
514 pub(crate) prerelease: Option<Prerelease>,
515 pub(crate) platform: Platform,
516 pub(crate) variant: PythonVariant,
517}
518
519impl PythonInstallationKey {
520 pub(crate) fn new(
521 implementation: LenientImplementationName,
522 major: u8,
523 minor: u8,
524 patch: u8,
525 prerelease: Option<Prerelease>,
526 platform: Platform,
527 variant: PythonVariant,
528 ) -> Self {
529 Self {
530 implementation,
531 major,
532 minor,
533 patch,
534 prerelease,
535 platform,
536 variant,
537 }
538 }
539
540 pub(crate) fn new_from_version(
541 implementation: LenientImplementationName,
542 version: &PythonVersion,
543 platform: Platform,
544 variant: PythonVariant,
545 ) -> Self {
546 Self {
547 implementation,
548 major: version.major(),
549 minor: version.minor(),
550 patch: version.patch().unwrap_or_default(),
551 prerelease: version.pre(),
552 platform,
553 variant,
554 }
555 }
556
557 pub fn implementation(&self) -> Cow<'_, LenientImplementationName> {
558 if self.os().is_emscripten() {
559 Cow::Owned(LenientImplementationName::from(ImplementationName::Pyodide))
560 } else {
561 Cow::Borrowed(&self.implementation)
562 }
563 }
564
565 pub fn version(&self) -> PythonVersion {
566 PythonVersion::from_str(&format!(
567 "{}.{}.{}{}",
568 self.major,
569 self.minor,
570 self.patch,
571 self.prerelease
572 .map(|pre| pre.to_string())
573 .unwrap_or_default()
574 ))
575 .expect("Python installation keys must have valid Python versions")
576 }
577
578 #[cfg(windows)]
580 pub(crate) fn sys_version(&self) -> String {
581 format!("{}.{}.{}", self.major, self.minor, self.patch)
582 }
583
584 pub fn major(&self) -> u8 {
585 self.major
586 }
587
588 pub fn minor(&self) -> u8 {
589 self.minor
590 }
591
592 pub(crate) fn prerelease(&self) -> Option<Prerelease> {
593 self.prerelease
594 }
595
596 pub(crate) fn platform(&self) -> &Platform {
597 &self.platform
598 }
599
600 pub fn arch(&self) -> &Arch {
601 &self.platform.arch
602 }
603
604 pub fn os(&self) -> &Os {
605 &self.platform.os
606 }
607
608 pub fn libc(&self) -> &Libc {
609 &self.platform.libc
610 }
611
612 pub fn variant(&self) -> &PythonVariant {
613 &self.variant
614 }
615
616 pub fn executable_name_minor(&self) -> String {
618 format!(
619 "{name}{maj}.{min}{var}{exe}",
620 name = self.implementation().executable_install_name(),
621 maj = self.major,
622 min = self.minor,
623 var = self.variant.executable_suffix(),
624 exe = std::env::consts::EXE_SUFFIX
625 )
626 }
627
628 pub fn executable_name_major(&self) -> String {
630 format!(
631 "{name}{maj}{var}{exe}",
632 name = self.implementation().executable_install_name(),
633 maj = self.major,
634 var = self.variant.executable_suffix(),
635 exe = std::env::consts::EXE_SUFFIX
636 )
637 }
638
639 pub fn executable_name(&self) -> String {
641 format!(
642 "{name}{var}{exe}",
643 name = self.implementation().executable_install_name(),
644 var = self.variant.executable_suffix(),
645 exe = std::env::consts::EXE_SUFFIX
646 )
647 }
648}
649
650impl fmt::Display for PythonInstallationKey {
651 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652 let variant = match self.variant {
653 PythonVariant::Default => String::new(),
654 _ => format!("+{}", self.variant),
655 };
656 write!(
657 f,
658 "{}-{}.{}.{}{}{}-{}",
659 self.implementation(),
660 self.major,
661 self.minor,
662 self.patch,
663 self.prerelease
664 .map(|pre| pre.to_string())
665 .unwrap_or_default(),
666 variant,
667 self.platform
668 )
669 }
670}
671
672impl FromStr for PythonInstallationKey {
673 type Err = PythonInstallationKeyError;
674
675 fn from_str(key: &str) -> Result<Self, Self::Err> {
676 let parts = key.split('-').collect::<Vec<_>>();
677
678 if parts.len() != 5 {
680 return Err(PythonInstallationKeyError::ParseError(
681 key.to_string(),
682 format!(
683 "expected exactly 5 `-`-separated values, got {}",
684 parts.len()
685 ),
686 ));
687 }
688
689 let [implementation_str, version_str, os, arch, libc] = parts.as_slice() else {
690 unreachable!()
691 };
692
693 let implementation = LenientImplementationName::from(*implementation_str);
694
695 let (version, variant) = match version_str.split_once('+') {
696 Some((version, variant)) => {
697 let variant = PythonVariant::from_str(variant).map_err(|()| {
698 PythonInstallationKeyError::ParseError(
699 key.to_string(),
700 format!("invalid Python variant: {variant}"),
701 )
702 })?;
703 (version, variant)
704 }
705 None => (*version_str, PythonVariant::Default),
706 };
707
708 let version = PythonVersion::from_str(version).map_err(|err| {
709 PythonInstallationKeyError::ParseError(
710 key.to_string(),
711 format!("invalid Python version: {err}"),
712 )
713 })?;
714
715 let platform = Platform::from_parts(os, arch, libc).map_err(|err| {
716 PythonInstallationKeyError::ParseError(
717 key.to_string(),
718 format!("invalid platform: {err}"),
719 )
720 })?;
721
722 Ok(Self {
723 implementation,
724 major: version.major(),
725 minor: version.minor(),
726 patch: version.patch().unwrap_or_default(),
727 prerelease: version.pre(),
728 platform,
729 variant,
730 })
731 }
732}
733
734impl PartialOrd for PythonInstallationKey {
735 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
736 Some(self.cmp(other))
737 }
738}
739
740impl Ord for PythonInstallationKey {
741 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
742 self.implementation
743 .cmp(&other.implementation)
744 .then_with(|| self.version().cmp(&other.version()))
745 .then_with(|| self.platform.cmp(&other.platform).reverse())
747 .then_with(|| self.variant.cmp(&other.variant).reverse())
749 }
750}
751
752#[derive(Clone, Eq, Ord, PartialOrd, RefCast)]
754#[repr(transparent)]
755pub struct PythonInstallationMinorVersionKey(PythonInstallationKey);
756
757impl PythonInstallationMinorVersionKey {
758 #[inline]
760 pub fn ref_cast(key: &PythonInstallationKey) -> &Self {
761 RefCast::ref_cast(key)
762 }
763
764 #[inline]
768 pub fn highest_installations_by_minor_version_key<'a, I>(
769 installations: I,
770 ) -> IndexMap<Self, ManagedPythonInstallation>
771 where
772 I: IntoIterator<Item = &'a ManagedPythonInstallation>,
773 {
774 let mut minor_versions = IndexMap::default();
775 for installation in installations {
776 minor_versions
777 .entry(installation.minor_version_key().clone())
778 .and_modify(|high_installation: &mut ManagedPythonInstallation| {
779 if installation.key() >= high_installation.key() {
780 *high_installation = installation.clone();
781 }
782 })
783 .or_insert_with(|| installation.clone());
784 }
785 minor_versions
786 }
787}
788
789impl fmt::Display for PythonInstallationMinorVersionKey {
790 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
791 let variant = match self.0.variant {
794 PythonVariant::Default => String::new(),
795 _ => format!("+{}", self.0.variant),
796 };
797 write!(
798 f,
799 "{}-{}.{}{}-{}",
800 self.0.implementation, self.0.major, self.0.minor, variant, self.0.platform,
801 )
802 }
803}
804
805impl fmt::Debug for PythonInstallationMinorVersionKey {
806 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
807 f.debug_struct("PythonInstallationMinorVersionKey")
810 .field("implementation", &self.0.implementation)
811 .field("major", &self.0.major)
812 .field("minor", &self.0.minor)
813 .field("variant", &self.0.variant)
814 .field("os", &self.0.platform.os)
815 .field("arch", &self.0.platform.arch)
816 .field("libc", &self.0.platform.libc)
817 .finish()
818 }
819}
820
821impl PartialEq for PythonInstallationMinorVersionKey {
822 fn eq(&self, other: &Self) -> bool {
823 self.0.implementation == other.0.implementation
826 && self.0.major == other.0.major
827 && self.0.minor == other.0.minor
828 && self.0.platform == other.0.platform
829 && self.0.variant == other.0.variant
830 }
831}
832
833impl Hash for PythonInstallationMinorVersionKey {
834 fn hash<H: Hasher>(&self, state: &mut H) {
835 self.0.implementation.hash(state);
838 self.0.major.hash(state);
839 self.0.minor.hash(state);
840 self.0.platform.hash(state);
841 self.0.variant.hash(state);
842 }
843}
844
845impl From<PythonInstallationKey> for PythonInstallationMinorVersionKey {
846 fn from(key: PythonInstallationKey) -> Self {
847 Self(key)
848 }
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854 use uv_platform::ArchVariant;
855
856 #[test]
857 fn test_python_installation_key_from_str() {
858 let key = PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64-gnu").unwrap();
860 assert_eq!(
861 key.implementation,
862 LenientImplementationName::Known(ImplementationName::CPython)
863 );
864 assert_eq!(key.major, 3);
865 assert_eq!(key.minor, 12);
866 assert_eq!(key.patch, 0);
867 assert_eq!(
868 key.platform.os,
869 Os::new(target_lexicon::OperatingSystem::Linux)
870 );
871 assert_eq!(
872 key.platform.arch,
873 Arch::new(target_lexicon::Architecture::X86_64, None)
874 );
875 assert_eq!(
876 key.platform.libc,
877 Libc::Some(target_lexicon::Environment::Gnu)
878 );
879
880 let key = PythonInstallationKey::from_str("cpython-3.11.2-linux-x86_64_v3-musl").unwrap();
882 assert_eq!(
883 key.implementation,
884 LenientImplementationName::Known(ImplementationName::CPython)
885 );
886 assert_eq!(key.major, 3);
887 assert_eq!(key.minor, 11);
888 assert_eq!(key.patch, 2);
889 assert_eq!(
890 key.platform.os,
891 Os::new(target_lexicon::OperatingSystem::Linux)
892 );
893 assert_eq!(
894 key.platform.arch,
895 Arch::new(target_lexicon::Architecture::X86_64, Some(ArchVariant::V3))
896 );
897 assert_eq!(
898 key.platform.libc,
899 Libc::Some(target_lexicon::Environment::Musl)
900 );
901
902 let key = PythonInstallationKey::from_str("cpython-3.13.0+freethreaded-macos-aarch64-none")
904 .unwrap();
905 assert_eq!(
906 key.implementation,
907 LenientImplementationName::Known(ImplementationName::CPython)
908 );
909 assert_eq!(key.major, 3);
910 assert_eq!(key.minor, 13);
911 assert_eq!(key.patch, 0);
912 assert_eq!(key.variant, PythonVariant::Freethreaded);
913 assert_eq!(
914 key.platform.os,
915 Os::new(target_lexicon::OperatingSystem::Darwin(None))
916 );
917 assert_eq!(
918 key.platform.arch,
919 Arch::new(
920 target_lexicon::Architecture::Aarch64(target_lexicon::Aarch64Architecture::Aarch64),
921 None
922 )
923 );
924 assert_eq!(key.platform.libc, Libc::None);
925
926 assert!(PythonInstallationKey::from_str("cpython-3.12.0-linux-x86_64").is_err());
928 assert!(PythonInstallationKey::from_str("cpython-3.12.0").is_err());
929 assert!(PythonInstallationKey::from_str("cpython").is_err());
930 }
931
932 #[test]
933 fn test_python_installation_key_display() {
934 let key = PythonInstallationKey {
935 implementation: LenientImplementationName::from("cpython"),
936 major: 3,
937 minor: 12,
938 patch: 0,
939 prerelease: None,
940 platform: Platform::from_str("linux-x86_64-gnu").unwrap(),
941 variant: PythonVariant::Default,
942 };
943 assert_eq!(key.to_string(), "cpython-3.12.0-linux-x86_64-gnu");
944
945 let key_with_variant = PythonInstallationKey {
946 implementation: LenientImplementationName::from("cpython"),
947 major: 3,
948 minor: 13,
949 patch: 0,
950 prerelease: None,
951 platform: Platform::from_str("macos-aarch64-none").unwrap(),
952 variant: PythonVariant::Freethreaded,
953 };
954 assert_eq!(
955 key_with_variant.to_string(),
956 "cpython-3.13.0+freethreaded-macos-aarch64-none"
957 );
958 }
959}