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