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