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