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