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