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