1use itertools::{Either, Itertools};
2use regex::Regex;
3use rustc_hash::{FxBuildHasher, FxHashSet};
4use same_file::is_same_file;
5use std::borrow::Cow;
6use std::env::consts::EXE_SUFFIX;
7use std::fmt::{self, Debug, Formatter};
8use std::{env, io, iter};
9use std::{path::Path, path::PathBuf, str::FromStr};
10use thiserror::Error;
11use tracing::{debug, instrument, trace};
12use uv_cache::Cache;
13use uv_client::BaseClientBuilder;
14use uv_distribution_types::RequiresPython;
15use uv_fs::Simplified;
16use uv_fs::which::is_executable;
17use uv_pep440::{
18 LowerBound, Prerelease, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
19 release_specifiers_to_ranges,
20};
21use uv_preview::Preview;
22use uv_static::EnvVars;
23use uv_warnings::{warn_user_once, write_warning_chain};
24use which::{which, which_all};
25
26use crate::downloads::{ManagedPythonDownloadList, PlatformRequest, PythonDownloadRequest};
27use crate::implementation::ImplementationName;
28use crate::installation::{PythonInstallation, PythonInstallationKey};
29use crate::interpreter::Error as InterpreterError;
30use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
31use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
32#[cfg(windows)]
33use crate::microsoft_store::find_microsoft_store_pythons;
34use crate::python_version::python_build_versions_from_env;
35use crate::virtualenv::Error as VirtualEnvError;
36use crate::virtualenv::{
37 CondaEnvironmentKind, conda_environment_from_env, virtualenv_from_env,
38 virtualenv_from_working_dir, virtualenv_python_executable,
39};
40#[cfg(windows)]
41use crate::windows_registry::{WindowsPython, registry_pythons};
42use crate::{BrokenLink, Interpreter, PythonVersion};
43
44#[derive(Debug, Clone, Eq, Default)]
48pub enum PythonRequest {
49 #[default]
54 Default,
55 Any,
57 Version(VersionRequest),
59 Directory(PathBuf),
61 File(PathBuf),
63 ExecutableName(String),
65 Implementation(ImplementationName),
67 ImplementationVersion(ImplementationName, VersionRequest),
69 Key(PythonDownloadRequest),
72}
73
74impl PartialEq for PythonRequest {
75 fn eq(&self, other: &Self) -> bool {
76 self.to_canonical_string() == other.to_canonical_string()
77 }
78}
79
80impl std::hash::Hash for PythonRequest {
81 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
82 self.to_canonical_string().hash(state);
83 }
84}
85
86impl<'a> serde::Deserialize<'a> for PythonRequest {
87 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
88 where
89 D: serde::Deserializer<'a>,
90 {
91 let s = <Cow<'_, str>>::deserialize(deserializer)?;
92 Ok(Self::parse(&s))
93 }
94}
95
96impl serde::Serialize for PythonRequest {
97 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
98 where
99 S: serde::Serializer,
100 {
101 let s = self.to_canonical_string();
102 serializer.serialize_str(&s)
103 }
104}
105
106#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
107#[serde(deny_unknown_fields, rename_all = "kebab-case")]
108#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
109#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
110pub enum PythonPreference {
111 OnlyManaged,
113 #[default]
114 Managed,
119 System,
123 OnlySystem,
125}
126
127#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
128#[serde(deny_unknown_fields, rename_all = "kebab-case")]
129#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
130#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
131pub enum PythonDownloads {
132 #[default]
134 #[serde(alias = "auto")]
135 Automatic,
136 Manual,
138 Never,
140}
141
142impl FromStr for PythonDownloads {
143 type Err = String;
144
145 fn from_str(s: &str) -> Result<Self, Self::Err> {
146 match s.to_ascii_lowercase().as_str() {
147 "auto" | "automatic" | "true" | "1" => Ok(Self::Automatic),
148 "manual" => Ok(Self::Manual),
149 "never" | "false" | "0" => Ok(Self::Never),
150 _ => Err(format!("Invalid value for `python-download`: '{s}'")),
151 }
152 }
153}
154
155impl From<bool> for PythonDownloads {
156 fn from(value: bool) -> Self {
157 if value { Self::Automatic } else { Self::Never }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
162pub enum EnvironmentPreference {
163 #[default]
165 OnlyVirtual,
166 ExplicitSystem,
168 OnlySystem,
170 Any,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Default)]
175pub(crate) struct DiscoveryPreferences {
176 python_preference: PythonPreference,
177 environment_preference: EnvironmentPreference,
178}
179
180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
181pub enum PythonVariant {
182 #[default]
183 Default,
184 Debug,
185 Freethreaded,
186 FreethreadedDebug,
187 Gil,
188 GilDebug,
189}
190
191#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
193pub enum VersionRequest {
194 #[default]
196 Default,
197 Any,
199 Major(u8, PythonVariant),
200 MajorMinor(u8, u8, PythonVariant),
201 MajorMinorPatch(u8, u8, u8, PythonVariant),
202 MajorMinorPrerelease(u8, u8, Prerelease, PythonVariant),
203 MajorMinorPatchPrerelease(u8, u8, u8, Prerelease, PythonVariant),
204 Range(VersionSpecifiers, PythonVariant),
205}
206
207type FindPythonResult = Result<PythonInstallation, PythonNotFound>;
211
212#[derive(Clone, Debug, Error)]
216pub struct PythonNotFound {
217 pub request: PythonRequest,
218 pub python_preference: PythonPreference,
219 pub environment_preference: EnvironmentPreference,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)]
224pub enum PythonSource {
225 ProvidedPath,
227 ActiveEnvironment,
229 CondaPrefix,
231 BaseCondaPrefix,
233 DiscoveredEnvironment,
235 SearchPath,
237 SearchPathFirst,
239 Registry,
241 MicrosoftStore,
243 Managed,
245 ParentInterpreter,
247}
248
249#[derive(Error, Debug)]
250pub enum Error {
251 #[error(transparent)]
252 Io(#[from] io::Error),
253
254 #[error("Failed to inspect Python interpreter from {} at `{}` ", _2, _1.user_display())]
256 Query(
257 #[source] Box<crate::interpreter::Error>,
258 PathBuf,
259 PythonSource,
260 ),
261
262 #[error("Failed to discover managed Python installations")]
265 ManagedPython(#[from] crate::managed::Error),
266
267 #[error(transparent)]
269 VirtualEnv(#[from] crate::virtualenv::Error),
270
271 #[cfg(windows)]
272 #[error("Failed to query installed Python versions from the Windows registry")]
273 RegistryError(#[from] windows::core::Error),
274
275 #[error(transparent)]
276 InvalidEnvironmentVariable(#[from] uv_static::InvalidEnvironmentVariable),
277
278 #[error("Invalid version request: {0}")]
280 InvalidVersionRequest(String),
281
282 #[error("Requesting the 'latest' Python version is not yet supported")]
284 LatestVersionRequest,
285
286 #[error("Interpreter discovery for `{0}` requires `{1}` but only `{2}` is allowed")]
288 SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
289
290 #[error(transparent)]
291 BuildVersion(#[from] crate::python_version::BuildVersionError),
292}
293
294impl uv_errors::Hint for Error {
295 fn hints(&self) -> uv_errors::Hints<'_> {
296 match self {
297 Self::Query(err, _, _) => err.hints(),
298 _ => uv_errors::Hints::none(),
299 }
300 }
301}
302
303fn python_executables_from_virtual_environments<'a>(
312 preview: Preview,
313) -> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a {
314 let from_active_environment = iter::once_with(|| {
315 virtualenv_from_env()
316 .into_iter()
317 .map(virtualenv_python_executable)
318 .map(|path| Ok((PythonSource::ActiveEnvironment, path)))
319 })
320 .flatten();
321
322 let from_conda_environment = iter::once_with(move || {
324 conda_environment_from_env(CondaEnvironmentKind::Child, preview)
325 .into_iter()
326 .map(virtualenv_python_executable)
327 .map(|path| Ok((PythonSource::CondaPrefix, path)))
328 })
329 .flatten();
330
331 let from_discovered_environment = iter::once_with(|| {
332 virtualenv_from_working_dir()
333 .map(|path| {
334 path.map(virtualenv_python_executable)
335 .map(|path| (PythonSource::DiscoveredEnvironment, path))
336 .into_iter()
337 })
338 .map_err(Error::from)
339 })
340 .flatten_ok();
341
342 from_active_environment
343 .chain(from_conda_environment)
344 .chain(from_discovered_environment)
345}
346
347fn python_executables_from_installed<'a>(
366 version: &'a VersionRequest,
367 implementation: Option<&'a ImplementationName>,
368 platform: PlatformRequest,
369 preference: PythonPreference,
370) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
371 let from_managed_installations = iter::once_with(move || {
372 ManagedPythonInstallations::from_settings(None)
373 .map_err(Error::from)
374 .and_then(|installed_installations| {
375 debug!(
376 "Searching for managed installations at `{}`",
377 installed_installations.root().user_display()
378 );
379 let installations = ManagedPythonInstallations::find_matching_current_platform()?;
380
381 let build_versions = python_build_versions_from_env()?;
382
383 Ok(installations
386 .into_iter()
387 .filter(move |installation| {
388 if !version.matches_version(&installation.version()) {
389 debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`");
390 return false;
391 }
392 if !platform.matches(installation.platform()) {
393 debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
394 return false;
395 }
396
397 if let Some(requested_build) = build_versions.get(&installation.implementation()) {
398 let Some(installation_build) = installation.build() else {
399 debug!(
400 "Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation"
401 );
402 return false;
403 };
404 if installation_build != requested_build {
405 debug!(
406 "Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`"
407 );
408 return false;
409 }
410 }
411
412 true
413 })
414 .inspect(|installation| debug!("Found managed installation `{installation}`"))
415 .map(move |installation| {
416 let executable = version
419 .patch()
420 .is_none()
421 .then(|| {
422 PythonMinorVersionLink::from_installation(
423 &installation,
424 )
425 .filter(PythonMinorVersionLink::exists)
426 .map(
427 |minor_version_link| {
428 minor_version_link.symlink_executable.clone()
429 },
430 )
431 })
432 .flatten()
433 .unwrap_or_else(|| installation.executable(false));
434 (PythonSource::Managed, executable)
435 })
436 )
437 })
438 })
439 .flatten_ok();
440
441 let from_search_path = iter::once_with(move || {
442 python_executables_from_search_path(version, implementation)
443 .enumerate()
444 .map(|(i, path)| {
445 if i == 0 {
446 Ok((PythonSource::SearchPathFirst, path))
447 } else {
448 Ok((PythonSource::SearchPath, path))
449 }
450 })
451 })
452 .flatten();
453
454 #[cfg(windows)]
455 let from_windows_registry: Box<
456 dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
457 > = match uv_static::parse_boolish_environment_variable(EnvVars::UV_PYTHON_NO_REGISTRY) {
458 Ok(Some(true)) => Box::new(iter::empty()),
459 Ok(Some(false) | None) => Box::new(
460 iter::once_with(move || {
461 let version_filter = move |entry: &WindowsPython| {
463 if let Some(found) = &entry.version {
464 if found.string.chars().filter(|c| *c == '.').count() == 1 {
466 version.matches_major_minor(found.major(), found.minor())
467 } else {
468 version.matches_version(found)
469 }
470 } else {
471 true
472 }
473 };
474
475 registry_pythons()
476 .map(|entries| {
477 entries
478 .into_iter()
479 .filter(version_filter)
480 .map(|entry| (PythonSource::Registry, entry.path))
481 .chain(
482 find_microsoft_store_pythons()
483 .filter(version_filter)
484 .map(|entry| (PythonSource::MicrosoftStore, entry.path)),
485 )
486 })
487 .map_err(Error::from)
488 })
489 .flatten_ok(),
490 ),
491 Err(err) => Box::new(iter::once(Err(Error::from(err)))),
492 };
493
494 #[cfg(not(windows))]
495 let from_windows_registry: Box<
496 dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
497 > = Box::new(iter::empty());
498
499 match preference {
500 PythonPreference::OnlyManaged => {
501 if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
505 Box::new(from_managed_installations.chain(from_search_path))
506 } else {
507 Box::new(from_managed_installations)
508 }
509 }
510 PythonPreference::Managed => Box::new(
511 from_managed_installations
512 .chain(from_search_path)
513 .chain(from_windows_registry),
514 ),
515 PythonPreference::System => Box::new(
516 from_search_path
517 .chain(from_windows_registry)
518 .chain(from_managed_installations),
519 ),
520 PythonPreference::OnlySystem => Box::new(from_search_path.chain(from_windows_registry)),
521 }
522}
523
524fn python_executables<'a>(
534 version: &'a VersionRequest,
535 implementation: Option<&'a ImplementationName>,
536 platform: PlatformRequest,
537 environments: EnvironmentPreference,
538 preference: PythonPreference,
539 preview: Preview,
540) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
541 let from_parent_interpreter = iter::once_with(|| {
543 env::var_os(EnvVars::UV_INTERNAL__PARENT_INTERPRETER)
544 .into_iter()
545 .map(|path| Ok((PythonSource::ParentInterpreter, PathBuf::from(path))))
546 })
547 .flatten();
548
549 let from_base_conda_environment = iter::once_with(move || {
551 conda_environment_from_env(CondaEnvironmentKind::Base, preview)
552 .into_iter()
553 .map(virtualenv_python_executable)
554 .map(|path| Ok((PythonSource::BaseCondaPrefix, path)))
555 })
556 .flatten();
557
558 let from_virtual_environments = python_executables_from_virtual_environments(preview);
559 let from_installed =
560 python_executables_from_installed(version, implementation, platform, preference);
561
562 match environments {
566 EnvironmentPreference::OnlyVirtual => {
567 Box::new(from_parent_interpreter.chain(from_virtual_environments))
568 }
569 EnvironmentPreference::ExplicitSystem | EnvironmentPreference::Any => Box::new(
570 from_parent_interpreter
571 .chain(from_virtual_environments)
572 .chain(from_base_conda_environment)
573 .chain(from_installed),
574 ),
575 EnvironmentPreference::OnlySystem => Box::new(
576 from_parent_interpreter
577 .chain(from_base_conda_environment)
578 .chain(from_installed),
579 ),
580 }
581}
582
583fn python_executables_from_search_path<'a>(
595 version: &'a VersionRequest,
596 implementation: Option<&'a ImplementationName>,
597) -> impl Iterator<Item = PathBuf> + 'a {
598 let search_path = env::var_os(EnvVars::UV_PYTHON_SEARCH_PATH)
600 .unwrap_or(env::var_os(EnvVars::PATH).unwrap_or_default());
601
602 let possible_names: Vec<_> = version
603 .executable_names(implementation)
604 .into_iter()
605 .map(|name| name.to_string())
606 .collect();
607
608 trace!(
609 "Searching PATH for executables: {}",
610 possible_names.join(", ")
611 );
612
613 let search_dirs: Vec<_> = env::split_paths(&search_path).collect();
617 let mut seen_dirs = FxHashSet::with_capacity_and_hasher(search_dirs.len(), FxBuildHasher);
618 search_dirs
619 .into_iter()
620 .filter(|dir| dir.is_dir())
621 .flat_map(move |dir| {
622 let dir_clone = dir.clone();
624 trace!(
625 "Checking `PATH` directory for interpreters: {}",
626 dir.display()
627 );
628 same_file::Handle::from_path(&dir)
629 .map(|handle| seen_dirs.insert(handle))
632 .inspect(|fresh_dir| {
633 if !fresh_dir {
634 trace!("Skipping already seen directory: {}", dir.display());
635 }
636 })
637 .unwrap_or(true)
639 .then(|| {
640 possible_names
641 .clone()
642 .into_iter()
643 .flat_map(move |name| {
644 which::which_in_global(&*name, Some(&dir))
646 .into_iter()
647 .flatten()
648 .collect::<Vec<_>>()
651 })
652 .chain(find_all_minor(implementation, version, &dir_clone))
653 .filter(|path| !is_windows_store_shim(path))
654 .inspect(|path| {
655 trace!("Found possible Python executable: {}", path.display());
656 })
657 .chain(
658 cfg!(windows)
660 .then(move || {
661 which::which_in_global("python.bat", Some(&dir_clone))
662 .into_iter()
663 .flatten()
664 .collect::<Vec<_>>()
665 })
666 .into_iter()
667 .flatten(),
668 )
669 })
670 .into_iter()
671 .flatten()
672 })
673}
674
675fn find_all_minor(
680 implementation: Option<&ImplementationName>,
681 version_request: &VersionRequest,
682 dir: &Path,
683) -> impl Iterator<Item = PathBuf> + use<> {
684 match version_request {
685 &VersionRequest::Any
686 | VersionRequest::Default
687 | VersionRequest::Major(_, _)
688 | VersionRequest::Range(_, _) => {
689 let regex = if let Some(implementation) = implementation {
690 Regex::new(&format!(
691 r"^({}|python3)\.(?<minor>\d\d?)t?{}$",
692 regex::escape(&implementation.to_string()),
693 regex::escape(EXE_SUFFIX)
694 ))
695 .unwrap()
696 } else {
697 Regex::new(&format!(
698 r"^python3\.(?<minor>\d\d?)t?{}$",
699 regex::escape(EXE_SUFFIX)
700 ))
701 .unwrap()
702 };
703 let all_minors = fs_err::read_dir(dir)
704 .into_iter()
705 .flatten()
706 .flatten()
707 .map(|entry| entry.path())
708 .filter(move |path| {
709 let Some(filename) = path.file_name() else {
710 return false;
711 };
712 let Some(filename) = filename.to_str() else {
713 return false;
714 };
715 let Some(captures) = regex.captures(filename) else {
716 return false;
717 };
718
719 let minor = captures["minor"].parse().ok();
721 if let Some(minor) = minor {
722 if minor < 6 {
724 return false;
725 }
726 if !version_request.matches_major_minor(3, minor) {
728 return false;
729 }
730 }
731 true
732 })
733 .filter(|path| is_executable(path))
734 .collect::<Vec<_>>();
735 Either::Left(all_minors.into_iter())
736 }
737 VersionRequest::MajorMinor(_, _, _)
738 | VersionRequest::MajorMinorPatch(_, _, _, _)
739 | VersionRequest::MajorMinorPrerelease(_, _, _, _)
740 | VersionRequest::MajorMinorPatchPrerelease(_, _, _, _, _) => Either::Right(iter::empty()),
741 }
742}
743
744fn python_installations<'a>(
754 version: &'a VersionRequest,
755 implementation: Option<&'a ImplementationName>,
756 platform: PlatformRequest,
757 environments: EnvironmentPreference,
758 preference: PythonPreference,
759 cache: &'a Cache,
760 preview: Preview,
761) -> impl Iterator<Item = Result<PythonInstallation, Error>> + 'a {
762 let installations = python_installations_from_executables(
763 python_executables(
767 version,
768 implementation,
769 platform,
770 environments,
771 preference,
772 preview,
773 )
774 .filter_ok(move |(source, path)| {
775 source_satisfies_environment_preference(*source, path, environments)
776 }),
777 cache,
778 )
779 .filter_ok(move |installation| {
780 interpreter_satisfies_environment_preference(
781 installation.source,
782 &installation.interpreter,
783 environments,
784 )
785 })
786 .filter_ok(move |installation| {
787 let request = version.clone().into_request_for_source(installation.source);
788 if request.matches_interpreter(&installation.interpreter) {
789 true
790 } else {
791 debug!(
792 "Skipping interpreter at `{}` from {}: does not satisfy request `{request}`",
793 installation.interpreter.sys_executable().user_display(),
794 installation.source,
795 );
796 false
797 }
798 })
799 .filter_ok(move |installation| preference.allows_installation(installation));
800
801 if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
802 Either::Left(installations.map_ok(|mut installation| {
803 if installation.interpreter.is_managed() {
806 installation.source = PythonSource::Managed;
807 }
808 installation
809 }))
810 } else {
811 Either::Right(installations)
812 }
813}
814
815fn python_installations_from_executables<'a>(
817 executables: impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a,
818 cache: &'a Cache,
819) -> impl Iterator<Item = Result<PythonInstallation, Error>> + 'a {
820 executables.map(|result| match result {
821 Ok((source, path)) => Interpreter::query(&path, cache)
822 .map(|interpreter| PythonInstallation {
823 source,
824 interpreter,
825 })
826 .inspect(|installation| {
827 debug!(
828 "Found `{}` at `{}` ({source})",
829 installation.key(),
830 path.display()
831 );
832 })
833 .map_err(|err| Error::Query(Box::new(err), path, source))
834 .inspect_err(|err| debug!("{err}")),
835 Err(err) => Err(err),
836 })
837}
838
839fn interpreter_satisfies_environment_preference(
846 source: PythonSource,
847 interpreter: &Interpreter,
848 preference: EnvironmentPreference,
849) -> bool {
850 match (
851 preference,
852 interpreter.is_virtualenv() || (matches!(source, PythonSource::CondaPrefix)),
854 ) {
855 (EnvironmentPreference::Any, _) => true,
856 (EnvironmentPreference::OnlyVirtual, true) => true,
857 (EnvironmentPreference::OnlyVirtual, false) => {
858 debug!(
859 "Ignoring Python interpreter at `{}`: only virtual environments allowed",
860 interpreter.sys_executable().display()
861 );
862 false
863 }
864 (EnvironmentPreference::ExplicitSystem, true) => true,
865 (EnvironmentPreference::ExplicitSystem, false) => {
866 if matches!(
867 source,
868 PythonSource::ProvidedPath | PythonSource::ParentInterpreter
869 ) {
870 debug!(
871 "Allowing explicitly requested system Python interpreter at `{}`",
872 interpreter.sys_executable().display()
873 );
874 true
875 } else {
876 debug!(
877 "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
878 interpreter.sys_executable().display()
879 );
880 false
881 }
882 }
883 (EnvironmentPreference::OnlySystem, true) => {
884 debug!(
885 "Ignoring Python interpreter at `{}`: system interpreter required",
886 interpreter.sys_executable().display()
887 );
888 false
889 }
890 (EnvironmentPreference::OnlySystem, false) => true,
891 }
892}
893
894fn source_satisfies_environment_preference(
901 source: PythonSource,
902 interpreter_path: &Path,
903 preference: EnvironmentPreference,
904) -> bool {
905 match preference {
906 EnvironmentPreference::Any => true,
907 EnvironmentPreference::OnlyVirtual => {
908 if source.is_maybe_virtualenv() {
909 true
910 } else {
911 debug!(
912 "Ignoring Python interpreter at `{}`: only virtual environments allowed",
913 interpreter_path.display()
914 );
915 false
916 }
917 }
918 EnvironmentPreference::ExplicitSystem => {
919 if source.is_maybe_virtualenv() {
920 true
921 } else {
922 debug!(
923 "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
924 interpreter_path.display()
925 );
926 false
927 }
928 }
929 EnvironmentPreference::OnlySystem => {
930 if source.is_maybe_system() {
931 true
932 } else {
933 debug!(
934 "Ignoring Python interpreter at `{}`: system interpreter required",
935 interpreter_path.display()
936 );
937 false
938 }
939 }
940 }
941}
942
943impl Error {
947 pub fn is_critical(&self) -> bool {
948 match self {
949 Self::Query(err, _, source) => match &**err {
952 InterpreterError::Encode(_)
953 | InterpreterError::Io(_)
954 | InterpreterError::SpawnFailed { .. } => true,
955 InterpreterError::UnexpectedResponse(UnexpectedResponseError { path, .. })
956 | InterpreterError::StatusCode(StatusCodeError { path, .. }) => {
957 debug!(
958 "Skipping bad interpreter at {} from {source}: {err}",
959 path.display()
960 );
961 false
962 }
963 InterpreterError::QueryScript { path, err } => {
964 debug!(
965 "Skipping bad interpreter at {} from {source}: {err}",
966 path.display()
967 );
968 false
969 }
970 #[cfg(windows)]
971 InterpreterError::CorruptWindowsPackage { path, err } => {
972 debug!(
973 "Skipping bad interpreter at {} from {source}: {err}",
974 path.display()
975 );
976 false
977 }
978 InterpreterError::PermissionDenied { path, err } => {
979 debug!(
980 "Skipping unexecutable interpreter at {} from {source}: {err}",
981 path.display()
982 );
983 false
984 }
985 InterpreterError::NotFound(path)
986 | InterpreterError::BrokenLink(BrokenLink { path, .. }) => {
987 if matches!(source, PythonSource::ActiveEnvironment)
990 && uv_fs::is_virtualenv_executable(path)
991 {
992 true
993 } else {
994 trace!("Skipping missing interpreter at {}", path.display());
995 false
996 }
997 }
998 },
999 Self::VirtualEnv(VirtualEnvError::MissingPyVenvCfg(path)) => {
1000 trace!("Skipping broken virtualenv at {}", path.display());
1001 false
1002 }
1003 _ => true,
1004 }
1005 }
1006}
1007
1008fn python_installation_from_executable(
1010 path: &PathBuf,
1011 cache: &Cache,
1012) -> Result<PythonInstallation, crate::interpreter::Error> {
1013 Ok(PythonInstallation {
1014 source: PythonSource::ProvidedPath,
1015 interpreter: Interpreter::query(path, cache)?,
1016 })
1017}
1018
1019fn python_installation_from_directory(
1021 path: &PathBuf,
1022 cache: &Cache,
1023) -> Result<PythonInstallation, crate::interpreter::Error> {
1024 let executable = virtualenv_python_executable(path);
1025 python_installation_from_executable(&executable, cache)
1026}
1027
1028fn python_installations_with_executable_name<'a>(
1030 name: &'a str,
1031 cache: &'a Cache,
1032) -> impl Iterator<Item = Result<PythonInstallation, Error>> + 'a {
1033 python_installations_from_executables(
1034 which_all(name)
1035 .into_iter()
1036 .flat_map(|inner| inner.map(|path| Ok((PythonSource::SearchPath, path)))),
1037 cache,
1038 )
1039}
1040
1041pub fn find_python_installations<'a>(
1043 request: &'a PythonRequest,
1044 environments: EnvironmentPreference,
1045 preference: PythonPreference,
1046 cache: &'a Cache,
1047 preview: Preview,
1048) -> Box<dyn Iterator<Item = Result<FindPythonResult, Error>> + 'a> {
1049 let sources = DiscoveryPreferences {
1050 python_preference: preference,
1051 environment_preference: environments,
1052 }
1053 .sources(request);
1054
1055 match request {
1056 PythonRequest::File(path) => Box::new(iter::once({
1057 if preference.allows_source(PythonSource::ProvidedPath) {
1058 debug!("Checking for Python interpreter at {request}");
1059 match python_installation_from_executable(path, cache) {
1060 Ok(installation) => Ok(Ok(installation)),
1061 Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1062 Ok(Err(PythonNotFound {
1063 request: request.clone(),
1064 python_preference: preference,
1065 environment_preference: environments,
1066 }))
1067 }
1068 Err(err) => Err(Error::Query(
1069 Box::new(err),
1070 path.clone(),
1071 PythonSource::ProvidedPath,
1072 )),
1073 }
1074 } else {
1075 Err(Error::SourceNotAllowed(
1076 request.clone(),
1077 PythonSource::ProvidedPath,
1078 preference,
1079 ))
1080 }
1081 })),
1082 PythonRequest::Directory(path) => Box::new(iter::once({
1083 if preference.allows_source(PythonSource::ProvidedPath) {
1084 debug!("Checking for Python interpreter in {request}");
1085 match python_installation_from_directory(path, cache) {
1086 Ok(installation) => Ok(Ok(installation)),
1087 Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1088 Ok(Err(PythonNotFound {
1089 request: request.clone(),
1090 python_preference: preference,
1091 environment_preference: environments,
1092 }))
1093 }
1094 Err(err) => Err(Error::Query(
1095 Box::new(err),
1096 path.clone(),
1097 PythonSource::ProvidedPath,
1098 )),
1099 }
1100 } else {
1101 Err(Error::SourceNotAllowed(
1102 request.clone(),
1103 PythonSource::ProvidedPath,
1104 preference,
1105 ))
1106 }
1107 })),
1108 PythonRequest::ExecutableName(name) => {
1109 if preference.allows_source(PythonSource::SearchPath) {
1110 debug!("Searching for Python interpreter with {request}");
1111 Box::new(
1112 python_installations_with_executable_name(name, cache)
1113 .filter_ok(move |installation| {
1114 interpreter_satisfies_environment_preference(
1115 installation.source,
1116 &installation.interpreter,
1117 environments,
1118 )
1119 })
1120 .map_ok(Ok),
1121 )
1122 } else {
1123 Box::new(iter::once(Err(Error::SourceNotAllowed(
1124 request.clone(),
1125 PythonSource::SearchPath,
1126 preference,
1127 ))))
1128 }
1129 }
1130 PythonRequest::Any => Box::new({
1131 debug!("Searching for any Python interpreter in {sources}");
1132 python_installations(
1133 &VersionRequest::Any,
1134 None,
1135 PlatformRequest::default(),
1136 environments,
1137 preference,
1138 cache,
1139 preview,
1140 )
1141 .map_ok(Ok)
1142 }),
1143 PythonRequest::Default => Box::new({
1144 debug!("Searching for default Python interpreter in {sources}");
1145 python_installations(
1146 &VersionRequest::Default,
1147 None,
1148 PlatformRequest::default(),
1149 environments,
1150 preference,
1151 cache,
1152 preview,
1153 )
1154 .map_ok(Ok)
1155 }),
1156 PythonRequest::Version(version) => {
1157 if let Err(err) = version.check_supported() {
1158 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1159 }
1160 Box::new({
1161 debug!("Searching for {request} in {sources}");
1162 python_installations(
1163 version,
1164 None,
1165 PlatformRequest::default(),
1166 environments,
1167 preference,
1168 cache,
1169 preview,
1170 )
1171 .map_ok(Ok)
1172 })
1173 }
1174 PythonRequest::Implementation(implementation) => Box::new({
1175 debug!("Searching for a {request} interpreter in {sources}");
1176 python_installations(
1177 &VersionRequest::Default,
1178 Some(implementation),
1179 PlatformRequest::default(),
1180 environments,
1181 preference,
1182 cache,
1183 preview,
1184 )
1185 .filter_ok(|installation| implementation.matches_interpreter(&installation.interpreter))
1186 .map_ok(Ok)
1187 }),
1188 PythonRequest::ImplementationVersion(implementation, version) => {
1189 if let Err(err) = version.check_supported() {
1190 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1191 }
1192 Box::new({
1193 debug!("Searching for {request} in {sources}");
1194 python_installations(
1195 version,
1196 Some(implementation),
1197 PlatformRequest::default(),
1198 environments,
1199 preference,
1200 cache,
1201 preview,
1202 )
1203 .filter_ok(|installation| {
1204 implementation.matches_interpreter(&installation.interpreter)
1205 })
1206 .map_ok(Ok)
1207 })
1208 }
1209 PythonRequest::Key(request) => {
1210 if let Some(version) = request.version() {
1211 if let Err(err) = version.check_supported() {
1212 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1213 }
1214 }
1215
1216 Box::new({
1217 debug!("Searching for {request} in {sources}");
1218 python_installations(
1219 request.version().unwrap_or(&VersionRequest::Default),
1220 request.implementation(),
1221 request.platform(),
1222 environments,
1223 preference,
1224 cache,
1225 preview,
1226 )
1227 .filter_ok(move |installation| {
1228 request.satisfied_by_interpreter(&installation.interpreter)
1229 })
1230 .map_ok(Ok)
1231 })
1232 }
1233 }
1234}
1235
1236pub(crate) fn find_python_installation(
1241 request: &PythonRequest,
1242 environments: EnvironmentPreference,
1243 preference: PythonPreference,
1244 cache: &Cache,
1245 preview: Preview,
1246) -> Result<FindPythonResult, Error> {
1247 let installations =
1248 find_python_installations(request, environments, preference, cache, preview);
1249 let mut first_prerelease = None;
1250 let mut first_debug = None;
1251 let mut first_managed = None;
1252 let mut first_error = None;
1253 for result in installations {
1254 if !result.as_ref().err().is_none_or(Error::is_critical) {
1256 if first_error.is_none() {
1258 if let Err(err) = result {
1259 first_error = Some(err);
1260 }
1261 }
1262 continue;
1263 }
1264
1265 let Ok(Ok(ref installation)) = result else {
1267 return result;
1268 };
1269
1270 let has_default_executable_name = installation.interpreter.has_default_executable_name()
1276 && matches!(
1277 installation.source,
1278 PythonSource::SearchPath | PythonSource::SearchPathFirst
1279 );
1280
1281 if installation.python_version().pre().is_some()
1284 && !request.allows_prereleases()
1285 && !installation.source.allows_prereleases()
1286 && !has_default_executable_name
1287 {
1288 debug!("Skipping pre-release installation {}", installation.key());
1289 if first_prerelease.is_none() {
1290 first_prerelease = Some(installation.clone());
1291 }
1292 continue;
1293 }
1294
1295 if installation.key().variant().is_debug()
1298 && !request.allows_debug()
1299 && !installation.source.allows_debug()
1300 && !has_default_executable_name
1301 {
1302 debug!("Skipping debug installation {}", installation.key());
1303 if first_debug.is_none() {
1304 first_debug = Some(installation.clone());
1305 }
1306 continue;
1307 }
1308
1309 if installation.is_alternative_implementation()
1314 && !request.allows_alternative_implementations()
1315 && !installation.source.allows_alternative_implementations()
1316 && !has_default_executable_name
1317 {
1318 debug!("Skipping alternative implementation {}", installation.key());
1319 continue;
1320 }
1321
1322 if matches!(preference, PythonPreference::System) && installation.is_managed() {
1325 debug!(
1326 "Skipping managed installation {}: system installation preferred",
1327 installation.key()
1328 );
1329 if first_managed.is_none() {
1330 first_managed = Some(installation.clone());
1331 }
1332 continue;
1333 }
1334
1335 return result;
1337 }
1338
1339 if let Some(installation) = first_managed {
1342 debug!(
1343 "Allowing managed installation {}: no system installations",
1344 installation.key()
1345 );
1346 return Ok(Ok(installation));
1347 }
1348
1349 if let Some(installation) = first_debug {
1352 debug!(
1353 "Allowing debug installation {}: no non-debug installations",
1354 installation.key()
1355 );
1356 return Ok(Ok(installation));
1357 }
1358
1359 if let Some(installation) = first_prerelease {
1361 debug!(
1362 "Allowing pre-release installation {}: no stable installations",
1363 installation.key()
1364 );
1365 return Ok(Ok(installation));
1366 }
1367
1368 if let Some(err) = first_error {
1371 return Err(err);
1372 }
1373
1374 Ok(Err(PythonNotFound {
1375 request: request.clone(),
1376 environment_preference: environments,
1377 python_preference: preference,
1378 }))
1379}
1380
1381#[instrument(skip_all, fields(request))]
1395pub(crate) async fn find_best_python_installation(
1396 request: &PythonRequest,
1397 environments: EnvironmentPreference,
1398 preference: PythonPreference,
1399 downloads_enabled: bool,
1400 client_builder: &BaseClientBuilder<'_>,
1401 cache: &Cache,
1402 reporter: Option<&dyn crate::downloads::Reporter>,
1403 python_install_mirror: Option<&str>,
1404 pypy_install_mirror: Option<&str>,
1405 python_downloads_json_url: Option<&str>,
1406 preview: Preview,
1407) -> Result<PythonInstallation, crate::Error> {
1408 debug!("Starting Python discovery for {request}");
1409 let original_request = request;
1410
1411 let mut previous_fetch_failed = false;
1412 let mut download_state = None;
1413
1414 let request_without_patch = match request {
1415 PythonRequest::Version(version) => {
1416 if version.has_patch() {
1417 Some(PythonRequest::Version(version.clone().without_patch()))
1418 } else {
1419 None
1420 }
1421 }
1422 PythonRequest::ImplementationVersion(implementation, version) => Some(
1423 PythonRequest::ImplementationVersion(*implementation, version.clone().without_patch()),
1424 ),
1425 _ => None,
1426 };
1427
1428 for (attempt, request) in iter::once(original_request)
1429 .chain(request_without_patch.iter())
1430 .chain(iter::once(&PythonRequest::Default))
1431 .enumerate()
1432 {
1433 debug!(
1434 "Looking for {request}{}",
1435 if request != original_request {
1436 format!(" attempt {attempt} (fallback after failing to find: {original_request})")
1437 } else {
1438 String::new()
1439 }
1440 );
1441 let result = find_python_installation(request, environments, preference, cache, preview);
1442 let error = match result {
1443 Ok(Ok(installation)) => {
1444 warn_on_unsupported_python(installation.interpreter());
1445 return Ok(installation);
1446 }
1447 Ok(Err(error)) => error.into(),
1449 Err(error) if !error.is_critical() => error.into(),
1450 Err(error) => return Err(error.into()),
1451 };
1452
1453 if downloads_enabled
1455 && !previous_fetch_failed
1456 && let Some(download_request) = PythonDownloadRequest::from_request(request)
1457 {
1458 let (client, retry_policy, download_list) =
1459 if let Some(download_state) = &mut download_state {
1460 download_state
1461 } else {
1462 let download_list_client = client_builder.build()?;
1463 let download_list = ManagedPythonDownloadList::new(
1464 &download_list_client,
1465 python_downloads_json_url,
1466 )
1467 .await?;
1468 let retry_policy = client_builder.retry_policy();
1469
1470 let client = client_builder.clone().retries(0).build()?;
1473 download_state.insert((client, retry_policy, download_list))
1474 };
1475
1476 let download = download_request
1477 .clone()
1478 .fill()
1479 .map(|request| download_list.find(&request));
1480
1481 let result = match download {
1482 Ok(Ok(download)) => PythonInstallation::fetch(
1483 download,
1484 client,
1485 retry_policy,
1486 cache,
1487 reporter,
1488 python_install_mirror,
1489 pypy_install_mirror,
1490 )
1491 .await
1492 .map(Some),
1493 Ok(Err(crate::downloads::Error::NoDownloadFound(_))) => Ok(None),
1494 Ok(Err(error)) => Err(error.into()),
1495 Err(error) => Err(error.into()),
1496 };
1497 if let Ok(Some(installation)) = result {
1498 return Ok(installation);
1499 }
1500 if let Err(error) = result {
1508 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1512 return Err(error);
1513 }
1514
1515 let error = anyhow::Error::from(error).context(format!(
1516 "A managed Python download is available for {request}, but an error occurred when attempting to download it."
1517 ));
1518 write_warning_chain(error.as_ref()).expect("writing to stderr should not fail");
1519 previous_fetch_failed = true;
1520 }
1521 }
1522
1523 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1529 return Err(match error {
1530 crate::Error::MissingPython(err, _) => PythonNotFound {
1531 request: original_request.clone(),
1533 python_preference: err.python_preference,
1534 environment_preference: err.environment_preference,
1535 }
1536 .into(),
1537 other => other,
1538 });
1539 }
1540 }
1541
1542 unreachable!("The loop should have terminated when it reached PythonRequest::Default");
1543}
1544
1545fn warn_on_unsupported_python(interpreter: &Interpreter) {
1547 if interpreter.python_tuple() < (3, 8) {
1549 warn_user_once!(
1550 "uv is only compatible with Python >=3.8, found Python {}",
1551 interpreter.python_version()
1552 );
1553 }
1554}
1555
1556#[cfg(windows)]
1573fn is_windows_store_shim(path: &Path) -> bool {
1574 use std::os::windows::fs::MetadataExt;
1575 use std::os::windows::prelude::OsStrExt;
1576 use windows::Win32::Foundation::CloseHandle;
1577 use windows::Win32::Storage::FileSystem::{
1578 CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
1579 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_MODE, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
1580 OPEN_EXISTING,
1581 };
1582 use windows::Win32::System::IO::DeviceIoControl;
1583 use windows::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
1584 use windows::core::PCWSTR;
1585
1586 if !path.is_absolute() {
1588 return false;
1589 }
1590
1591 let mut components = path.components().rev();
1594
1595 if !components
1597 .next()
1598 .and_then(|component| component.as_os_str().to_str())
1599 .is_some_and(|component| {
1600 component.starts_with("python")
1601 && std::path::Path::new(component)
1602 .extension()
1603 .is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
1604 })
1605 {
1606 return false;
1607 }
1608
1609 if components
1611 .next()
1612 .is_none_or(|component| component.as_os_str() != "WindowsApps")
1613 {
1614 return false;
1615 }
1616
1617 if components
1619 .next()
1620 .is_none_or(|component| component.as_os_str() != "Microsoft")
1621 {
1622 return false;
1623 }
1624
1625 let Ok(md) = fs_err::symlink_metadata(path) else {
1627 return false;
1628 };
1629 if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 {
1630 return false;
1631 }
1632
1633 let mut path_encoded = path
1634 .as_os_str()
1635 .encode_wide()
1636 .chain(std::iter::once(0))
1637 .collect::<Vec<_>>();
1638
1639 #[allow(unsafe_code)]
1641 let reparse_handle = unsafe {
1642 CreateFileW(
1643 PCWSTR(path_encoded.as_mut_ptr()),
1644 0,
1645 FILE_SHARE_MODE(0),
1646 None,
1647 OPEN_EXISTING,
1648 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
1649 None,
1650 )
1651 };
1652
1653 let Ok(reparse_handle) = reparse_handle else {
1654 return false;
1655 };
1656
1657 let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
1658 let mut bytes_returned = 0;
1659
1660 #[allow(unsafe_code, clippy::cast_possible_truncation)]
1662 let success = unsafe {
1663 DeviceIoControl(
1664 reparse_handle,
1665 FSCTL_GET_REPARSE_POINT,
1666 None,
1667 0,
1668 Some(buf.as_mut_ptr().cast()),
1669 buf.len() as u32 * 2,
1670 Some(&raw mut bytes_returned),
1671 None,
1672 )
1673 .is_ok()
1674 };
1675
1676 #[allow(unsafe_code)]
1678 unsafe {
1679 let _ = CloseHandle(reparse_handle);
1680 }
1681
1682 if !success {
1684 return false;
1685 }
1686
1687 let reparse_point = String::from_utf16_lossy(&buf[..bytes_returned as usize]);
1688 reparse_point.contains("\\AppInstallerPythonRedirector.exe")
1689}
1690
1691#[cfg(not(windows))]
1695fn is_windows_store_shim(_path: &Path) -> bool {
1696 false
1697}
1698
1699impl PythonVariant {
1700 fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
1701 match self {
1702 Self::Default => {
1703 if (interpreter.python_major(), interpreter.python_minor()) >= (3, 14) {
1706 true
1709 } else {
1710 !interpreter.gil_disabled()
1713 }
1714 }
1715 Self::Debug => interpreter.debug_enabled(),
1716 Self::Freethreaded => interpreter.gil_disabled(),
1717 Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
1718 Self::Gil => !interpreter.gil_disabled(),
1719 Self::GilDebug => !interpreter.gil_disabled() && interpreter.debug_enabled(),
1720 }
1721 }
1722
1723 pub fn executable_suffix(self) -> &'static str {
1727 match self {
1728 Self::Default => "",
1729 Self::Debug => "d",
1730 Self::Freethreaded => "t",
1731 Self::FreethreadedDebug => "td",
1732 Self::Gil => "",
1733 Self::GilDebug => "d",
1734 }
1735 }
1736
1737 pub fn display_suffix(self) -> &'static str {
1739 match self {
1740 Self::Default => "",
1741 Self::Debug => "+debug",
1742 Self::Freethreaded => "+freethreaded",
1743 Self::FreethreadedDebug => "+freethreaded+debug",
1744 Self::Gil => "+gil",
1745 Self::GilDebug => "+gil+debug",
1746 }
1747 }
1748
1749 pub(crate) fn lib_suffix(self) -> &'static str {
1752 match self {
1753 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "",
1754 Self::Freethreaded | Self::FreethreadedDebug => "t",
1755 }
1756 }
1757
1758 pub fn is_freethreaded(self) -> bool {
1759 match self {
1760 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false,
1761 Self::Freethreaded | Self::FreethreadedDebug => true,
1762 }
1763 }
1764
1765 pub fn is_debug(self) -> bool {
1766 match self {
1767 Self::Default | Self::Freethreaded | Self::Gil => false,
1768 Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true,
1769 }
1770 }
1771}
1772impl PythonRequest {
1773 pub fn from_requires_python(requires_python: &RequiresPython) -> Option<Self> {
1775 let specifiers = requires_python.specifiers().clone();
1776 if specifiers.is_empty() {
1777 return None;
1778 }
1779
1780 Some(Self::Version(VersionRequest::from_specifiers(
1781 specifiers,
1782 PythonVariant::Default,
1783 )))
1784 }
1785
1786 pub fn parse(value: &str) -> Self {
1794 let lowercase_value = &value.to_ascii_lowercase();
1795
1796 if lowercase_value == "any" {
1798 return Self::Any;
1799 }
1800 if lowercase_value == "default" {
1801 return Self::Default;
1802 }
1803
1804 let abstract_version_prefixes = ["python", ""];
1806 let all_implementation_names =
1807 ImplementationName::long_names().chain(ImplementationName::short_names());
1808 if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1811 abstract_version_prefixes,
1812 all_implementation_names,
1813 lowercase_value,
1814 ) {
1815 return request;
1816 }
1817
1818 let value_as_path = PathBuf::from(value);
1819 if value_as_path.is_dir() {
1821 return Self::Directory(value_as_path);
1822 }
1823 if value_as_path.is_file() {
1825 return Self::File(value_as_path);
1826 }
1827
1828 #[cfg(windows)]
1830 if value_as_path.extension().is_none() {
1831 let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1832 if value_as_path.is_file() {
1833 return Self::File(value_as_path);
1834 }
1835 }
1836
1837 #[cfg(test)]
1842 if value_as_path.is_relative() {
1843 if let Ok(current_dir) = crate::current_dir() {
1844 let relative = current_dir.join(&value_as_path);
1845 if relative.is_dir() {
1846 return Self::Directory(relative);
1847 }
1848 if relative.is_file() {
1849 return Self::File(relative);
1850 }
1851 }
1852 }
1853 if value.contains(std::path::MAIN_SEPARATOR) {
1856 return Self::File(value_as_path);
1857 }
1858 if cfg!(windows) && value.contains('/') {
1861 return Self::File(value_as_path);
1862 }
1863 if let Ok(request) = PythonDownloadRequest::from_str(value) {
1864 return Self::Key(request);
1865 }
1866 Self::ExecutableName(value.to_string())
1869 }
1870
1871 pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
1885 let lowercase_value = &value.to_ascii_lowercase();
1886 let abstract_version_prefixes = if cfg!(windows) {
1888 &["python", "pythonw"][..]
1889 } else {
1890 &["python"][..]
1891 };
1892 if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
1894 return Ok(Some(Self::Default));
1895 }
1896 Self::parse_versions_and_implementations(
1897 abstract_version_prefixes.iter().copied(),
1898 ImplementationName::long_names(),
1899 lowercase_value,
1900 )
1901 }
1902
1903 fn parse_versions_and_implementations<'a>(
1912 abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
1914 implementation_names: impl IntoIterator<Item = &'a str>,
1916 lowercase_value: &str,
1918 ) -> Result<Option<Self>, Error> {
1919 for prefix in abstract_version_prefixes {
1920 if let Some(version_request) =
1921 Self::try_split_prefix_and_version(prefix, lowercase_value)?
1922 {
1923 return Ok(Some(Self::Version(version_request)));
1927 }
1928 }
1929 for implementation in implementation_names {
1930 if lowercase_value == implementation {
1931 return Ok(Some(Self::Implementation(
1932 ImplementationName::from_str(implementation).unwrap(),
1935 )));
1936 }
1937 if let Some(version_request) =
1938 Self::try_split_prefix_and_version(implementation, lowercase_value)?
1939 {
1940 return Ok(Some(Self::ImplementationVersion(
1942 ImplementationName::from_str(implementation).unwrap(),
1944 version_request,
1945 )));
1946 }
1947 }
1948 Ok(None)
1949 }
1950
1951 fn try_split_prefix_and_version(
1962 prefix: &str,
1963 lowercase_value: &str,
1964 ) -> Result<Option<VersionRequest>, Error> {
1965 if lowercase_value.starts_with('@') {
1966 return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
1967 }
1968 let Some(rest) = lowercase_value.strip_prefix(prefix) else {
1969 return Ok(None);
1970 };
1971 if rest.is_empty() {
1973 return Ok(None);
1974 }
1975 if let Some(after_at) = rest.strip_prefix('@') {
1978 if after_at == "latest" {
1979 return Err(Error::LatestVersionRequest);
1982 }
1983 return after_at.parse().map(Some);
1984 }
1985 Ok(rest.parse().ok())
1988 }
1989
1990 pub fn includes_patch(&self) -> bool {
1992 match self {
1993 Self::Default => false,
1994 Self::Any => false,
1995 Self::Version(version_request) => version_request.patch().is_some(),
1996 Self::Directory(..) => false,
1997 Self::File(..) => false,
1998 Self::ExecutableName(..) => false,
1999 Self::Implementation(..) => false,
2000 Self::ImplementationVersion(_, version) => version.patch().is_some(),
2001 Self::Key(request) => request
2002 .version
2003 .as_ref()
2004 .is_some_and(|request| request.patch().is_some()),
2005 }
2006 }
2007
2008 pub fn includes_prerelease(&self) -> bool {
2010 match self {
2011 Self::Default => false,
2012 Self::Any => false,
2013 Self::Version(version_request) => version_request.prerelease().is_some(),
2014 Self::Directory(..) => false,
2015 Self::File(..) => false,
2016 Self::ExecutableName(..) => false,
2017 Self::Implementation(..) => false,
2018 Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
2019 Self::Key(request) => request
2020 .version
2021 .as_ref()
2022 .is_some_and(|request| request.prerelease().is_some()),
2023 }
2024 }
2025
2026 pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
2028 fn is_same_executable(path1: &Path, path2: &Path) -> bool {
2030 path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
2031 }
2032
2033 match self {
2034 Self::Default | Self::Any => true,
2035 Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2036 Self::Directory(directory) => {
2037 is_same_executable(directory, interpreter.sys_prefix())
2039 || is_same_executable(
2040 virtualenv_python_executable(directory).as_path(),
2041 interpreter.sys_executable(),
2042 )
2043 }
2044 Self::File(file) => {
2045 if is_same_executable(interpreter.sys_executable(), file) {
2047 return true;
2048 }
2049 if interpreter
2051 .sys_base_executable()
2052 .is_some_and(|sys_base_executable| {
2053 is_same_executable(sys_base_executable, file)
2054 })
2055 {
2056 return true;
2057 }
2058 if cfg!(windows) {
2063 if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2064 if let (Some(file_base), Some(interpreter_base)) = (
2065 file_interpreter.sys_base_executable(),
2066 interpreter.sys_base_executable(),
2067 ) {
2068 if is_same_executable(file_base, interpreter_base) {
2069 return true;
2070 }
2071 }
2072 }
2073 }
2074 false
2075 }
2076 Self::ExecutableName(name) => {
2077 if interpreter
2079 .sys_executable()
2080 .file_name()
2081 .is_some_and(|filename| filename == name.as_str())
2082 {
2083 return true;
2084 }
2085 if interpreter
2087 .sys_base_executable()
2088 .and_then(|executable| executable.file_name())
2089 .is_some_and(|file_name| file_name == name.as_str())
2090 {
2091 return true;
2092 }
2093 if which(name)
2096 .ok()
2097 .as_ref()
2098 .and_then(|executable| executable.file_name())
2099 .is_some_and(|file_name| file_name == name.as_str())
2100 {
2101 return true;
2102 }
2103 false
2104 }
2105 Self::Implementation(implementation) => interpreter
2106 .implementation_name()
2107 .eq_ignore_ascii_case(implementation.into()),
2108 Self::ImplementationVersion(implementation, version) => {
2109 version.matches_interpreter(interpreter)
2110 && interpreter
2111 .implementation_name()
2112 .eq_ignore_ascii_case(implementation.into())
2113 }
2114 Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2115 }
2116 }
2117
2118 pub(crate) fn allows_prereleases(&self) -> bool {
2120 match self {
2121 Self::Default => false,
2122 Self::Any => true,
2123 Self::Version(version) => version.allows_prereleases(),
2124 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2125 Self::Implementation(_) => false,
2126 Self::ImplementationVersion(_, _) => true,
2127 Self::Key(request) => request.allows_prereleases(),
2128 }
2129 }
2130
2131 pub(crate) fn allows_debug(&self) -> bool {
2133 match self {
2134 Self::Default => false,
2135 Self::Any => true,
2136 Self::Version(version) => version.is_debug(),
2137 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2138 Self::Implementation(_) => false,
2139 Self::ImplementationVersion(_, _) => true,
2140 Self::Key(request) => request.allows_debug(),
2141 }
2142 }
2143
2144 pub(crate) fn allows_alternative_implementations(&self) -> bool {
2146 match self {
2147 Self::Default => false,
2148 Self::Any => true,
2149 Self::Version(_) => false,
2150 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2151 Self::Implementation(implementation)
2152 | Self::ImplementationVersion(implementation, _) => {
2153 !matches!(implementation, ImplementationName::CPython)
2154 }
2155 Self::Key(request) => request.allows_alternative_implementations(),
2156 }
2157 }
2158
2159 pub(crate) fn is_explicit_system(&self) -> bool {
2160 matches!(self, Self::File(_) | Self::Directory(_))
2161 }
2162
2163 pub fn to_canonical_string(&self) -> String {
2167 match self {
2168 Self::Any => "any".to_string(),
2169 Self::Default => "default".to_string(),
2170 Self::Version(version) => version.to_string(),
2171 Self::Directory(path) => path.display().to_string(),
2172 Self::File(path) => path.display().to_string(),
2173 Self::ExecutableName(name) => name.clone(),
2174 Self::Implementation(implementation) => implementation.to_string(),
2175 Self::ImplementationVersion(implementation, version) => {
2176 format!("{implementation}@{version}")
2177 }
2178 Self::Key(request) => request.to_string(),
2179 }
2180 }
2181
2182 pub fn as_pep440_version(&self) -> Option<Version> {
2186 match self {
2187 Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2188 Self::Key(download_request) => download_request
2189 .version()
2190 .and_then(VersionRequest::as_pep440_version),
2191 Self::Default
2192 | Self::Any
2193 | Self::Directory(_)
2194 | Self::File(_)
2195 | Self::ExecutableName(_)
2196 | Self::Implementation(_) => None,
2197 }
2198 }
2199
2200 fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2206 match self {
2207 Self::Version(version) | Self::ImplementationVersion(_, version) => {
2208 version.as_version_specifiers()
2209 }
2210 Self::Key(download_request) => download_request
2211 .version()
2212 .and_then(VersionRequest::as_version_specifiers),
2213 Self::Default
2214 | Self::Any
2215 | Self::Directory(_)
2216 | Self::File(_)
2217 | Self::ExecutableName(_)
2218 | Self::Implementation(_) => None,
2219 }
2220 }
2221
2222 pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2228 let Some(specifiers) = self.as_version_specifiers() else {
2229 return true;
2230 };
2231
2232 let request_range = release_specifiers_to_ranges(specifiers);
2233 let requires_python_range =
2234 release_specifiers_to_ranges(requires_python.specifiers().clone());
2235 !request_range
2236 .intersection(&requires_python_range)
2237 .is_empty()
2238 }
2239}
2240
2241impl PythonSource {
2242 pub fn is_managed(self) -> bool {
2243 matches!(self, Self::Managed)
2244 }
2245
2246 pub(crate) fn allows_prereleases(self) -> bool {
2248 match self {
2249 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2250 Self::SearchPath
2251 | Self::SearchPathFirst
2252 | Self::CondaPrefix
2253 | Self::BaseCondaPrefix
2254 | Self::ProvidedPath
2255 | Self::ParentInterpreter
2256 | Self::ActiveEnvironment
2257 | Self::DiscoveredEnvironment => true,
2258 }
2259 }
2260
2261 pub(crate) fn allows_debug(self) -> bool {
2263 match self {
2264 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2265 Self::SearchPath
2266 | Self::SearchPathFirst
2267 | Self::CondaPrefix
2268 | Self::BaseCondaPrefix
2269 | Self::ProvidedPath
2270 | Self::ParentInterpreter
2271 | Self::ActiveEnvironment
2272 | Self::DiscoveredEnvironment => true,
2273 }
2274 }
2275
2276 pub(crate) fn allows_alternative_implementations(self) -> bool {
2278 match self {
2279 Self::Managed
2280 | Self::Registry
2281 | Self::SearchPath
2282 | Self::SearchPathFirst
2285 | Self::MicrosoftStore => false,
2286 Self::CondaPrefix
2287 | Self::BaseCondaPrefix
2288 | Self::ProvidedPath
2289 | Self::ParentInterpreter
2290 | Self::ActiveEnvironment
2291 | Self::DiscoveredEnvironment => true,
2292 }
2293 }
2294
2295 fn is_maybe_virtualenv(self) -> bool {
2307 match self {
2308 Self::ProvidedPath
2309 | Self::ActiveEnvironment
2310 | Self::DiscoveredEnvironment
2311 | Self::CondaPrefix
2312 | Self::BaseCondaPrefix
2313 | Self::ParentInterpreter
2314 | Self::SearchPathFirst => true,
2315 Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2316 }
2317 }
2318
2319 fn is_explicit(self) -> bool {
2322 match self {
2323 Self::ProvidedPath
2324 | Self::ParentInterpreter
2325 | Self::ActiveEnvironment
2326 | Self::CondaPrefix => true,
2327 Self::Managed
2328 | Self::DiscoveredEnvironment
2329 | Self::SearchPath
2330 | Self::SearchPathFirst
2331 | Self::Registry
2332 | Self::MicrosoftStore
2333 | Self::BaseCondaPrefix => false,
2334 }
2335 }
2336
2337 fn is_maybe_system(self) -> bool {
2339 match self {
2340 Self::CondaPrefix
2341 | Self::BaseCondaPrefix
2342 | Self::ParentInterpreter
2343 | Self::ProvidedPath
2344 | Self::Managed
2345 | Self::SearchPath
2346 | Self::SearchPathFirst
2347 | Self::Registry
2348 | Self::MicrosoftStore => true,
2349 Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2350 }
2351 }
2352}
2353
2354impl PythonPreference {
2355 fn allows_source(self, source: PythonSource) -> bool {
2356 if !matches!(
2358 source,
2359 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2360 ) {
2361 return true;
2362 }
2363
2364 match self {
2365 Self::OnlyManaged => matches!(source, PythonSource::Managed),
2366 Self::Managed | Self::System => matches!(
2367 source,
2368 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2369 ),
2370 Self::OnlySystem => {
2371 matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2372 }
2373 }
2374 }
2375
2376 pub(crate) fn allows_managed(self) -> bool {
2377 match self {
2378 Self::OnlySystem => false,
2379 Self::Managed | Self::System | Self::OnlyManaged => true,
2380 }
2381 }
2382
2383 fn allows_interpreter(self, interpreter: &Interpreter) -> bool {
2388 match self {
2389 Self::OnlyManaged => interpreter.is_managed(),
2390 Self::OnlySystem => !interpreter.is_managed(),
2391 Self::Managed | Self::System => true,
2392 }
2393 }
2394
2395 pub fn allows_installation(self, installation: &PythonInstallation) -> bool {
2403 let source = installation.source;
2404 let interpreter = &installation.interpreter;
2405
2406 match self {
2407 Self::OnlyManaged => {
2408 if self.allows_interpreter(interpreter) {
2409 true
2410 } else if source.is_explicit() {
2411 debug!(
2412 "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2413 interpreter.sys_executable().display()
2414 );
2415 true
2416 } else {
2417 debug!(
2418 "Ignoring Python interpreter at `{}`: only managed interpreters allowed",
2419 interpreter.sys_executable().display()
2420 );
2421 false
2422 }
2423 }
2424 Self::Managed | Self::System => true,
2426 Self::OnlySystem => {
2427 if self.allows_interpreter(interpreter) {
2428 true
2429 } else if source.is_explicit() {
2430 debug!(
2431 "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2432 interpreter.sys_executable().display()
2433 );
2434 true
2435 } else {
2436 debug!(
2437 "Ignoring Python interpreter at `{}`: only system interpreters allowed",
2438 interpreter.sys_executable().display()
2439 );
2440 false
2441 }
2442 }
2443 }
2444 }
2445
2446 #[must_use]
2451 pub fn with_system_flag(self, system: bool) -> Self {
2452 match self {
2453 Self::OnlyManaged => self,
2458 Self::Managed => {
2459 if system {
2460 Self::System
2461 } else {
2462 self
2463 }
2464 }
2465 Self::System => self,
2466 Self::OnlySystem => self,
2467 }
2468 }
2469}
2470
2471impl PythonDownloads {
2472 pub fn is_automatic(self) -> bool {
2473 matches!(self, Self::Automatic)
2474 }
2475}
2476
2477impl EnvironmentPreference {
2478 pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2479 match (system, mutable) {
2480 (true, _) => Self::OnlySystem,
2482 (false, true) => Self::ExplicitSystem,
2484 (false, false) => Self::Any,
2486 }
2487 }
2488}
2489
2490#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2491pub(crate) struct ExecutableName {
2492 implementation: Option<ImplementationName>,
2493 major: Option<u8>,
2494 minor: Option<u8>,
2495 patch: Option<u8>,
2496 prerelease: Option<Prerelease>,
2497 variant: PythonVariant,
2498}
2499
2500#[derive(Debug, Clone, PartialEq, Eq)]
2501struct ExecutableNameComparator<'a> {
2502 name: ExecutableName,
2503 request: &'a VersionRequest,
2504 implementation: Option<&'a ImplementationName>,
2505}
2506
2507impl Ord for ExecutableNameComparator<'_> {
2508 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2512 let name_ordering = if self.implementation.is_some() {
2515 std::cmp::Ordering::Greater
2516 } else {
2517 std::cmp::Ordering::Less
2518 };
2519 if self.name.implementation.is_none() && other.name.implementation.is_some() {
2520 return name_ordering.reverse();
2521 }
2522 if self.name.implementation.is_some() && other.name.implementation.is_none() {
2523 return name_ordering;
2524 }
2525 let ordering = self.name.implementation.cmp(&other.name.implementation);
2527 if ordering != std::cmp::Ordering::Equal {
2528 return ordering;
2529 }
2530 let ordering = self.name.major.cmp(&other.name.major);
2531 let is_default_request =
2532 matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2533 if ordering != std::cmp::Ordering::Equal {
2534 return if is_default_request {
2535 ordering.reverse()
2536 } else {
2537 ordering
2538 };
2539 }
2540 let ordering = self.name.minor.cmp(&other.name.minor);
2541 if ordering != std::cmp::Ordering::Equal {
2542 return if is_default_request {
2543 ordering.reverse()
2544 } else {
2545 ordering
2546 };
2547 }
2548 let ordering = self.name.patch.cmp(&other.name.patch);
2549 if ordering != std::cmp::Ordering::Equal {
2550 return if is_default_request {
2551 ordering.reverse()
2552 } else {
2553 ordering
2554 };
2555 }
2556 let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2557 if ordering != std::cmp::Ordering::Equal {
2558 return if is_default_request {
2559 ordering.reverse()
2560 } else {
2561 ordering
2562 };
2563 }
2564 let ordering = self.name.variant.cmp(&other.name.variant);
2565 if ordering != std::cmp::Ordering::Equal {
2566 return if is_default_request {
2567 ordering.reverse()
2568 } else {
2569 ordering
2570 };
2571 }
2572 ordering
2573 }
2574}
2575
2576impl PartialOrd for ExecutableNameComparator<'_> {
2577 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2578 Some(self.cmp(other))
2579 }
2580}
2581
2582impl ExecutableName {
2583 #[must_use]
2584 fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2585 self.implementation = Some(implementation);
2586 self
2587 }
2588
2589 #[must_use]
2590 fn with_major(mut self, major: u8) -> Self {
2591 self.major = Some(major);
2592 self
2593 }
2594
2595 #[must_use]
2596 fn with_minor(mut self, minor: u8) -> Self {
2597 self.minor = Some(minor);
2598 self
2599 }
2600
2601 #[must_use]
2602 fn with_patch(mut self, patch: u8) -> Self {
2603 self.patch = Some(patch);
2604 self
2605 }
2606
2607 #[must_use]
2608 fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2609 self.prerelease = Some(prerelease);
2610 self
2611 }
2612
2613 #[must_use]
2614 fn with_variant(mut self, variant: PythonVariant) -> Self {
2615 self.variant = variant;
2616 self
2617 }
2618
2619 fn into_comparator<'a>(
2620 self,
2621 request: &'a VersionRequest,
2622 implementation: Option<&'a ImplementationName>,
2623 ) -> ExecutableNameComparator<'a> {
2624 ExecutableNameComparator {
2625 name: self,
2626 request,
2627 implementation,
2628 }
2629 }
2630}
2631
2632impl fmt::Display for ExecutableName {
2633 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2634 if let Some(implementation) = self.implementation {
2635 write!(f, "{implementation}")?;
2636 } else {
2637 f.write_str("python")?;
2638 }
2639 if let Some(major) = self.major {
2640 write!(f, "{major}")?;
2641 if let Some(minor) = self.minor {
2642 write!(f, ".{minor}")?;
2643 if let Some(patch) = self.patch {
2644 write!(f, ".{patch}")?;
2645 }
2646 }
2647 }
2648 if let Some(prerelease) = &self.prerelease {
2649 write!(f, "{prerelease}")?;
2650 }
2651 f.write_str(self.variant.executable_suffix())?;
2652 f.write_str(EXE_SUFFIX)?;
2653 Ok(())
2654 }
2655}
2656
2657impl VersionRequest {
2658 pub fn from_specifiers(specifiers: VersionSpecifiers, variant: PythonVariant) -> Self {
2663 if let [specifier] = specifiers.iter().as_slice() {
2664 if specifier.operator() == &uv_pep440::Operator::Equal {
2665 if let Ok(request) = Self::from_str(&specifier.version().to_string()) {
2666 return request;
2667 }
2668 }
2669 }
2670 Self::Range(specifiers, variant)
2671 }
2672
2673 #[must_use]
2675 pub fn only_minor(self) -> Self {
2676 match self {
2677 Self::Any => self,
2678 Self::Default => self,
2679 Self::Range(specifiers, variant) => Self::Range(
2680 specifiers
2681 .into_iter()
2682 .map(|s| s.only_minor_release())
2683 .collect(),
2684 variant,
2685 ),
2686 Self::Major(..) => self,
2687 Self::MajorMinor(..) => self,
2688 Self::MajorMinorPatch(major, minor, _, variant)
2689 | Self::MajorMinorPrerelease(major, minor, _, variant)
2690 | Self::MajorMinorPatchPrerelease(major, minor, _, _, variant) => {
2691 Self::MajorMinor(major, minor, variant)
2692 }
2693 }
2694 }
2695
2696 pub(crate) fn executable_names(
2698 &self,
2699 implementation: Option<&ImplementationName>,
2700 ) -> Vec<ExecutableName> {
2701 let prerelease = match self {
2702 Self::MajorMinorPrerelease(_, _, prerelease, _)
2703 | Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => {
2704 Some(prerelease)
2706 }
2707 _ => None,
2708 };
2709
2710 let mut names = Vec::new();
2712 names.push(ExecutableName::default());
2713
2714 if let Some(major) = self.major() {
2716 names.push(ExecutableName::default().with_major(major));
2718 if let Some(minor) = self.minor() {
2719 names.push(
2721 ExecutableName::default()
2722 .with_major(major)
2723 .with_minor(minor),
2724 );
2725 if let Some(patch) = self.patch() {
2726 names.push(
2728 ExecutableName::default()
2729 .with_major(major)
2730 .with_minor(minor)
2731 .with_patch(patch),
2732 );
2733 }
2734 }
2735 } else {
2736 names.push(ExecutableName::default().with_major(3));
2738 }
2739
2740 if let Some(prerelease) = prerelease {
2741 for i in 0..names.len() {
2743 let name = names[i];
2744 if name.minor.is_none() {
2745 continue;
2748 }
2749 names.push(name.with_prerelease(*prerelease));
2750 }
2751 }
2752
2753 if let Some(implementation) = implementation {
2755 for i in 0..names.len() {
2756 let name = names[i].with_implementation(*implementation);
2757 names.push(name);
2758 }
2759 } else {
2760 if matches!(self, Self::Any) {
2762 for i in 0..names.len() {
2763 for implementation in ImplementationName::iter_all() {
2764 let name = names[i].with_implementation(implementation);
2765 names.push(name);
2766 }
2767 }
2768 }
2769 }
2770
2771 if let Some(variant) = self.variant() {
2773 if variant != PythonVariant::Default {
2774 for i in 0..names.len() {
2775 let name = names[i].with_variant(variant);
2776 names.push(name);
2777 }
2778 }
2779 }
2780
2781 names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
2782 names.reverse();
2783
2784 names
2785 }
2786
2787 pub(crate) fn major(&self) -> Option<u8> {
2789 match self {
2790 Self::Any | Self::Default | Self::Range(_, _) => None,
2791 Self::Major(major, _) => Some(*major),
2792 Self::MajorMinor(major, _, _) => Some(*major),
2793 Self::MajorMinorPatch(major, _, _, _) => Some(*major),
2794 Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2795 Self::MajorMinorPatchPrerelease(major, _, _, _, _) => Some(*major),
2796 }
2797 }
2798
2799 pub(crate) fn minor(&self) -> Option<u8> {
2801 match self {
2802 Self::Any | Self::Default | Self::Range(_, _) => None,
2803 Self::Major(_, _) => None,
2804 Self::MajorMinor(_, minor, _) => Some(*minor),
2805 Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
2806 Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2807 Self::MajorMinorPatchPrerelease(_, minor, _, _, _) => Some(*minor),
2808 }
2809 }
2810
2811 pub(crate) fn patch(&self) -> Option<u8> {
2813 match self {
2814 Self::Any | Self::Default | Self::Range(_, _) => None,
2815 Self::Major(_, _) => None,
2816 Self::MajorMinor(_, _, _) => None,
2817 Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
2818 Self::MajorMinorPrerelease(_, _, _, _) => None,
2819 Self::MajorMinorPatchPrerelease(_, _, patch, _, _) => Some(*patch),
2820 }
2821 }
2822
2823 pub(crate) fn prerelease(&self) -> Option<&Prerelease> {
2825 match self {
2826 Self::Any | Self::Default | Self::Range(_, _) => None,
2827 Self::Major(_, _) => None,
2828 Self::MajorMinor(_, _, _) => None,
2829 Self::MajorMinorPatch(_, _, _, _) => None,
2830 Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
2831 Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => Some(prerelease),
2832 }
2833 }
2834
2835 fn check_supported(&self) -> Result<(), String> {
2839 match self {
2840 Self::Any | Self::Default => (),
2841 Self::Major(major, _) => {
2842 if *major < 3 {
2843 return Err(format!(
2844 "Python <3 is not supported but {major} was requested."
2845 ));
2846 }
2847 }
2848 Self::MajorMinor(major, minor, _) => {
2849 if (*major, *minor) < (3, 6) {
2850 return Err(format!(
2851 "Python <3.6 is not supported but {major}.{minor} was requested."
2852 ));
2853 }
2854 }
2855 Self::MajorMinorPatch(major, minor, patch, _) => {
2856 if (*major, *minor) < (3, 6) {
2857 return Err(format!(
2858 "Python <3.6 is not supported but {major}.{minor}.{patch} was requested."
2859 ));
2860 }
2861 }
2862 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2863 if (*major, *minor) < (3, 6) {
2864 return Err(format!(
2865 "Python <3.6 is not supported but {major}.{minor}{prerelease} was requested."
2866 ));
2867 }
2868 }
2869 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
2870 if (*major, *minor) < (3, 6) {
2871 return Err(format!(
2872 "Python <3.6 is not supported but {major}.{minor}.{patch}{prerelease} was requested."
2873 ));
2874 }
2875 }
2876 Self::Range(_, _) => (),
2878 }
2879
2880 if self.is_freethreaded() {
2881 if let Self::MajorMinor(major, minor, _) = self.clone().without_patch() {
2882 if (major, minor) < (3, 13) {
2883 return Err(format!(
2884 "Python <3.13 does not support free-threading but {self} was requested."
2885 ));
2886 }
2887 }
2888 }
2889
2890 Ok(())
2891 }
2892
2893 #[must_use]
2899 fn into_request_for_source(self, source: PythonSource) -> Self {
2900 match self {
2901 Self::Default => match source {
2902 PythonSource::ParentInterpreter
2903 | PythonSource::CondaPrefix
2904 | PythonSource::BaseCondaPrefix
2905 | PythonSource::ProvidedPath
2906 | PythonSource::DiscoveredEnvironment
2907 | PythonSource::ActiveEnvironment => Self::Any,
2908 PythonSource::SearchPath
2909 | PythonSource::SearchPathFirst
2910 | PythonSource::Registry
2911 | PythonSource::MicrosoftStore
2912 | PythonSource::Managed => Self::Default,
2913 },
2914 _ => self,
2915 }
2916 }
2917
2918 pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
2920 match self {
2921 Self::Any => true,
2922 Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
2924 Self::Major(major, variant) => {
2925 interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
2926 }
2927 Self::MajorMinor(major, minor, variant) => {
2928 (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
2929 && variant.matches_interpreter(interpreter)
2930 }
2931 Self::MajorMinorPatch(major, minor, patch, variant) => {
2932 (
2933 interpreter.python_major(),
2934 interpreter.python_minor(),
2935 interpreter.python_patch(),
2936 ) == (*major, *minor, *patch)
2937 && interpreter.python_version().pre().is_none()
2940 && variant.matches_interpreter(interpreter)
2941 }
2942 Self::Range(specifiers, variant) => {
2943 let version = if specifiers
2946 .iter()
2947 .any(uv_pep440::VersionSpecifier::any_prerelease)
2948 {
2949 Cow::Borrowed(interpreter.python_version())
2950 } else {
2951 Cow::Owned(interpreter.python_version().only_release())
2952 };
2953 specifiers.contains(&version) && variant.matches_interpreter(interpreter)
2954 }
2955 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
2956 let version = interpreter.python_version();
2957 let Some(interpreter_prerelease) = version.pre() else {
2958 return false;
2959 };
2960 (
2961 interpreter.python_major(),
2962 interpreter.python_minor(),
2963 interpreter_prerelease,
2964 ) == (*major, *minor, *prerelease)
2965 && variant.matches_interpreter(interpreter)
2966 }
2967 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
2968 let version = interpreter.python_version();
2969 let Some(interpreter_prerelease) = version.pre() else {
2970 return false;
2971 };
2972 (
2973 interpreter.python_major(),
2974 interpreter.python_minor(),
2975 interpreter.python_patch(),
2976 interpreter_prerelease,
2977 ) == (*major, *minor, *patch, *prerelease)
2978 && variant.matches_interpreter(interpreter)
2979 }
2980 }
2981 }
2982
2983 pub(crate) fn matches_version(&self, version: &PythonVersion) -> bool {
2988 match self {
2989 Self::Any | Self::Default => true,
2990 Self::Major(major, _) => version.major() == *major,
2991 Self::MajorMinor(major, minor, _) => {
2992 (version.major(), version.minor()) == (*major, *minor)
2993 }
2994 Self::MajorMinorPatch(major, minor, patch, _) => {
2995 (version.major(), version.minor(), version.patch())
2996 == (*major, *minor, Some(*patch))
2997 }
2998 Self::Range(specifiers, _) => {
2999 let version = if specifiers
3002 .iter()
3003 .any(uv_pep440::VersionSpecifier::any_prerelease)
3004 {
3005 Cow::Borrowed(&version.version)
3006 } else {
3007 Cow::Owned(version.version.only_release())
3008 };
3009 specifiers.contains(&version)
3010 }
3011 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3012 (version.major(), version.minor(), version.pre())
3013 == (*major, *minor, Some(*prerelease))
3014 }
3015 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3016 (
3017 version.major(),
3018 version.minor(),
3019 version.patch(),
3020 version.pre(),
3021 ) == (*major, *minor, Some(*patch), Some(*prerelease))
3022 }
3023 }
3024 }
3025
3026 fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
3031 match self {
3032 Self::Any | Self::Default => true,
3033 Self::Major(self_major, _) => *self_major == major,
3034 Self::MajorMinor(self_major, self_minor, _) => {
3035 (*self_major, *self_minor) == (major, minor)
3036 }
3037 Self::MajorMinorPatch(self_major, self_minor, _, _) => {
3038 (*self_major, *self_minor) == (major, minor)
3039 }
3040 Self::Range(specifiers, _) => {
3041 let range = release_specifiers_to_ranges(specifiers.clone());
3042 let Some((lower, upper)) = range.bounding_range() else {
3043 return true;
3044 };
3045 let version = Version::new([u64::from(major), u64::from(minor)]);
3046
3047 let lower = LowerBound::new(lower.cloned());
3048 if !lower.major_minor().contains(&version) {
3049 return false;
3050 }
3051
3052 let upper = UpperBound::new(upper.cloned());
3053 if !upper.major_minor().contains(&version) {
3054 return false;
3055 }
3056
3057 true
3058 }
3059 Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
3060 (*self_major, *self_minor) == (major, minor)
3061 }
3062 Self::MajorMinorPatchPrerelease(self_major, self_minor, _, _, _) => {
3063 (*self_major, *self_minor) == (major, minor)
3064 }
3065 }
3066 }
3067
3068 pub(crate) fn matches_major_minor_patch_prerelease(
3074 &self,
3075 major: u8,
3076 minor: u8,
3077 patch: u8,
3078 prerelease: Option<Prerelease>,
3079 ) -> bool {
3080 match self {
3081 Self::Any | Self::Default => true,
3082 Self::Major(self_major, _) => *self_major == major,
3083 Self::MajorMinor(self_major, self_minor, _) => {
3084 (*self_major, *self_minor) == (major, minor)
3085 }
3086 Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
3087 (*self_major, *self_minor, *self_patch) == (major, minor, patch)
3088 && prerelease.is_none()
3091 }
3092 Self::Range(specifiers, _) => specifiers.contains(
3093 &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
3094 .with_pre(prerelease),
3095 ),
3096 Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3097 (*self_major, *self_minor, 0, Some(*self_prerelease))
3099 == (major, minor, patch, prerelease)
3100 }
3101 Self::MajorMinorPatchPrerelease(
3102 self_major,
3103 self_minor,
3104 self_patch,
3105 self_prerelease,
3106 _,
3107 ) => {
3108 (
3109 *self_major,
3110 *self_minor,
3111 *self_patch,
3112 Some(*self_prerelease),
3113 ) == (major, minor, patch, prerelease)
3114 }
3115 }
3116 }
3117
3118 pub(crate) fn matches_installation_key(&self, key: &PythonInstallationKey) -> bool {
3123 self.matches_major_minor_patch_prerelease(key.major, key.minor, key.patch, key.prerelease())
3124 }
3125
3126 fn has_patch(&self) -> bool {
3128 match self {
3129 Self::Any | Self::Default => false,
3130 Self::Major(..) => false,
3131 Self::MajorMinor(..) => false,
3132 Self::MajorMinorPatch(..) => true,
3133 Self::MajorMinorPrerelease(..) => false,
3134 Self::MajorMinorPatchPrerelease(..) => true,
3135 Self::Range(_, _) => false,
3136 }
3137 }
3138
3139 #[must_use]
3143 fn without_patch(self) -> Self {
3144 match self {
3145 Self::Default => Self::Default,
3146 Self::Any => Self::Any,
3147 Self::Major(major, variant) => Self::Major(major, variant),
3148 Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3149 Self::MajorMinorPatch(major, minor, _, variant) => {
3150 Self::MajorMinor(major, minor, variant)
3151 }
3152 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3153 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3154 }
3155 Self::MajorMinorPatchPrerelease(major, minor, _, prerelease, variant) => {
3156 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3157 }
3158 Self::Range(_, _) => self,
3159 }
3160 }
3161
3162 pub(crate) fn allows_prereleases(&self) -> bool {
3164 match self {
3165 Self::Default => false,
3166 Self::Any => true,
3167 Self::Major(..) => false,
3168 Self::MajorMinor(..) => false,
3169 Self::MajorMinorPatch(..) => false,
3170 Self::MajorMinorPrerelease(..) => true,
3171 Self::MajorMinorPatchPrerelease(..) => true,
3172 Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3173 }
3174 }
3175
3176 pub(crate) fn is_debug(&self) -> bool {
3178 match self {
3179 Self::Any | Self::Default => false,
3180 Self::Major(_, variant)
3181 | Self::MajorMinor(_, _, variant)
3182 | Self::MajorMinorPatch(_, _, _, variant)
3183 | Self::MajorMinorPrerelease(_, _, _, variant)
3184 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3185 | Self::Range(_, variant) => variant.is_debug(),
3186 }
3187 }
3188
3189 pub(crate) fn is_freethreaded(&self) -> bool {
3191 match self {
3192 Self::Any | Self::Default => false,
3193 Self::Major(_, variant)
3194 | Self::MajorMinor(_, _, variant)
3195 | Self::MajorMinorPatch(_, _, _, variant)
3196 | Self::MajorMinorPrerelease(_, _, _, variant)
3197 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3198 | Self::Range(_, variant) => variant.is_freethreaded(),
3199 }
3200 }
3201
3202 pub(crate) fn variant(&self) -> Option<PythonVariant> {
3204 match self {
3205 Self::Any => None,
3206 Self::Default => Some(PythonVariant::Default),
3207 Self::Major(_, variant)
3208 | Self::MajorMinor(_, _, variant)
3209 | Self::MajorMinorPatch(_, _, _, variant)
3210 | Self::MajorMinorPrerelease(_, _, _, variant)
3211 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3212 | Self::Range(_, variant) => Some(*variant),
3213 }
3214 }
3215
3216 pub fn as_pep440_version(&self) -> Option<Version> {
3220 match self {
3221 Self::Default | Self::Any | Self::Range(_, _) => None,
3222 Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3223 Self::MajorMinor(major, minor, _) => {
3224 Some(Version::new([u64::from(*major), u64::from(*minor)]))
3225 }
3226 Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3227 u64::from(*major),
3228 u64::from(*minor),
3229 u64::from(*patch),
3230 ])),
3231 Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3233 Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3234 ),
3235 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => Some(
3236 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3237 .with_pre(Some(*prerelease)),
3238 ),
3239 }
3240 }
3241
3242 fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
3248 match self {
3249 Self::Default | Self::Any => None,
3250 Self::Major(major, _) => Some(VersionSpecifiers::from(
3251 VersionSpecifier::equals_star_version(Version::new([u64::from(*major)])),
3252 )),
3253 Self::MajorMinor(major, minor, _) => Some(VersionSpecifiers::from(
3254 VersionSpecifier::equals_star_version(Version::new([
3255 u64::from(*major),
3256 u64::from(*minor),
3257 ])),
3258 )),
3259 Self::MajorMinorPatch(major, minor, patch, _) => {
3260 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3261 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)]),
3262 )))
3263 }
3264 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3265 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3266 Version::new([u64::from(*major), u64::from(*minor), 0])
3267 .with_pre(Some(*prerelease)),
3268 )))
3269 }
3270 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3271 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3272 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3273 .with_pre(Some(*prerelease)),
3274 )))
3275 }
3276 Self::Range(specifiers, _) => Some(specifiers.clone()),
3277 }
3278 }
3279}
3280
3281impl FromStr for VersionRequest {
3282 type Err = Error;
3283
3284 fn from_str(s: &str) -> Result<Self, Self::Err> {
3285 fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3288 if s.chars().all(char::is_alphabetic) {
3290 return Err(Error::InvalidVersionRequest(s.to_string()));
3291 }
3292
3293 let Some(mut start) = s.rfind(|c: char| c.is_numeric()) else {
3294 return Ok((s, PythonVariant::Default));
3295 };
3296
3297 start += 1;
3299
3300 if start + 1 > s.len() {
3302 return Ok((s, PythonVariant::Default));
3303 }
3304
3305 let variant = &s[start..];
3306 let prefix = &s[..start];
3307
3308 let variant = variant.strip_prefix('+').unwrap_or(variant);
3310
3311 let Ok(variant) = PythonVariant::from_str(variant) else {
3315 return Ok((s, PythonVariant::Default));
3316 };
3317
3318 Ok((prefix, variant))
3319 }
3320
3321 let (s, variant) = parse_variant(s)?;
3322 let Ok(version) = Version::from_str(s) else {
3323 return parse_version_specifiers_request(s, variant);
3324 };
3325
3326 let version = split_wheel_tag_release_version(version);
3328
3329 if version.post().is_some() || version.dev().is_some() {
3331 return Err(Error::InvalidVersionRequest(s.to_string()));
3332 }
3333
3334 if !version.local().is_empty() {
3337 return Err(Error::InvalidVersionRequest(s.to_string()));
3338 }
3339
3340 let Ok(release) = try_into_u8_slice(&version.release()) else {
3342 return Err(Error::InvalidVersionRequest(s.to_string()));
3343 };
3344
3345 let prerelease = version.pre();
3346
3347 match release.as_slice() {
3348 [major] => {
3350 if prerelease.is_some() {
3352 return Err(Error::InvalidVersionRequest(s.to_string()));
3353 }
3354 Ok(Self::Major(*major, variant))
3355 }
3356 [major, minor] => {
3358 if let Some(prerelease) = prerelease {
3359 return Ok(Self::MajorMinorPrerelease(
3360 *major, *minor, prerelease, variant,
3361 ));
3362 }
3363 Ok(Self::MajorMinor(*major, *minor, variant))
3364 }
3365 [major, minor, patch] => {
3367 if let Some(prerelease) = prerelease {
3368 if *patch == 0 {
3369 return Ok(Self::MajorMinorPrerelease(
3370 *major, *minor, prerelease, variant,
3371 ));
3372 }
3373 return Ok(Self::MajorMinorPatchPrerelease(
3374 *major, *minor, *patch, prerelease, variant,
3375 ));
3376 }
3377 Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3378 }
3379 _ => Err(Error::InvalidVersionRequest(s.to_string())),
3380 }
3381 }
3382}
3383
3384impl FromStr for PythonVariant {
3385 type Err = ();
3386
3387 fn from_str(s: &str) -> Result<Self, Self::Err> {
3388 match s {
3389 "t" | "freethreaded" => Ok(Self::Freethreaded),
3390 "d" | "debug" => Ok(Self::Debug),
3391 "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3392 "gil" => Ok(Self::Gil),
3393 "gil+debug" => Ok(Self::GilDebug),
3394 "" => Ok(Self::Default),
3395 _ => Err(()),
3396 }
3397 }
3398}
3399
3400impl fmt::Display for PythonVariant {
3401 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3402 match self {
3403 Self::Default => f.write_str("default"),
3404 Self::Debug => f.write_str("debug"),
3405 Self::Freethreaded => f.write_str("freethreaded"),
3406 Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3407 Self::Gil => f.write_str("gil"),
3408 Self::GilDebug => f.write_str("gil+debug"),
3409 }
3410 }
3411}
3412
3413fn parse_version_specifiers_request(
3414 s: &str,
3415 variant: PythonVariant,
3416) -> Result<VersionRequest, Error> {
3417 let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3418 return Err(Error::InvalidVersionRequest(s.to_string()));
3419 };
3420 if specifiers.is_empty() {
3421 return Err(Error::InvalidVersionRequest(s.to_string()));
3422 }
3423 Ok(VersionRequest::from_specifiers(specifiers, variant))
3424}
3425
3426impl From<&PythonVersion> for VersionRequest {
3427 fn from(version: &PythonVersion) -> Self {
3428 Self::from_str(&version.string)
3429 .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3430 }
3431}
3432
3433impl fmt::Display for VersionRequest {
3434 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3435 match self {
3436 Self::Any => f.write_str("any"),
3437 Self::Default => f.write_str("default"),
3438 Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3439 Self::MajorMinor(major, minor, variant) => {
3440 write!(f, "{major}.{minor}{}", variant.display_suffix())
3441 }
3442 Self::MajorMinorPatch(major, minor, patch, variant) => {
3443 write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3444 }
3445 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3446 write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3447 }
3448 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3449 write!(
3450 f,
3451 "{major}.{minor}.{patch}{prerelease}{}",
3452 variant.display_suffix()
3453 )
3454 }
3455 Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3456 }
3457 }
3458}
3459
3460impl fmt::Display for PythonRequest {
3461 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3462 match self {
3463 Self::Default => write!(f, "a default Python"),
3464 Self::Any => write!(f, "any Python"),
3465 Self::Version(version) => write!(f, "Python {version}"),
3466 Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3467 Self::File(path) => write!(f, "path `{}`", path.user_display()),
3468 Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3469 Self::Implementation(implementation) => {
3470 write!(f, "{}", implementation.pretty())
3471 }
3472 Self::ImplementationVersion(implementation, version) => {
3473 write!(f, "{} {version}", implementation.pretty())
3474 }
3475 Self::Key(request) => write!(f, "{request}"),
3476 }
3477 }
3478}
3479
3480impl fmt::Display for PythonSource {
3481 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3482 match self {
3483 Self::ProvidedPath => f.write_str("provided path"),
3484 Self::ActiveEnvironment => f.write_str("active virtual environment"),
3485 Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3486 Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3487 Self::SearchPath => f.write_str("search path"),
3488 Self::SearchPathFirst => f.write_str("first executable in the search path"),
3489 Self::Registry => f.write_str("registry"),
3490 Self::MicrosoftStore => f.write_str("Microsoft Store"),
3491 Self::Managed => f.write_str("managed installations"),
3492 Self::ParentInterpreter => f.write_str("parent interpreter"),
3493 }
3494 }
3495}
3496
3497impl PythonPreference {
3498 fn sources(self) -> &'static [PythonSource] {
3501 match self {
3502 Self::OnlyManaged => &[PythonSource::Managed],
3503 Self::Managed => {
3504 if cfg!(windows) {
3505 &[
3506 PythonSource::Managed,
3507 PythonSource::SearchPath,
3508 PythonSource::Registry,
3509 ]
3510 } else {
3511 &[PythonSource::Managed, PythonSource::SearchPath]
3512 }
3513 }
3514 Self::System => {
3515 if cfg!(windows) {
3516 &[
3517 PythonSource::SearchPath,
3518 PythonSource::Registry,
3519 PythonSource::Managed,
3520 ]
3521 } else {
3522 &[PythonSource::SearchPath, PythonSource::Managed]
3523 }
3524 }
3525 Self::OnlySystem => {
3526 if cfg!(windows) {
3527 &[PythonSource::SearchPath, PythonSource::Registry]
3528 } else {
3529 &[PythonSource::SearchPath]
3530 }
3531 }
3532 }
3533 }
3534}
3535
3536impl fmt::Display for PythonPreference {
3537 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3538 f.write_str(match self {
3539 Self::OnlyManaged => "only managed",
3540 Self::Managed => "prefer managed",
3541 Self::System => "prefer system",
3542 Self::OnlySystem => "only system",
3543 })
3544 }
3545}
3546
3547impl DiscoveryPreferences {
3548 fn sources(&self, request: &PythonRequest) -> String {
3551 let python_sources = self
3552 .python_preference
3553 .sources()
3554 .iter()
3555 .map(ToString::to_string)
3556 .collect::<Vec<_>>();
3557 match self.environment_preference {
3558 EnvironmentPreference::Any => disjunction(
3559 &["virtual environments"]
3560 .into_iter()
3561 .chain(python_sources.iter().map(String::as_str))
3562 .collect::<Vec<_>>(),
3563 ),
3564 EnvironmentPreference::ExplicitSystem => {
3565 if request.is_explicit_system() {
3566 disjunction(
3567 &["virtual environments"]
3568 .into_iter()
3569 .chain(python_sources.iter().map(String::as_str))
3570 .collect::<Vec<_>>(),
3571 )
3572 } else {
3573 disjunction(&["virtual environments"])
3574 }
3575 }
3576 EnvironmentPreference::OnlySystem => disjunction(
3577 &python_sources
3578 .iter()
3579 .map(String::as_str)
3580 .collect::<Vec<_>>(),
3581 ),
3582 EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3583 }
3584 }
3585}
3586
3587impl fmt::Display for PythonNotFound {
3588 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3589 let sources = DiscoveryPreferences {
3590 python_preference: self.python_preference,
3591 environment_preference: self.environment_preference,
3592 }
3593 .sources(&self.request);
3594
3595 match self.request {
3596 PythonRequest::Default | PythonRequest::Any => {
3597 write!(f, "No interpreter found in {sources}")
3598 }
3599 PythonRequest::File(_) => {
3600 write!(f, "No interpreter found at {}", self.request)
3601 }
3602 PythonRequest::Directory(_) => {
3603 write!(f, "No interpreter found in {}", self.request)
3604 }
3605 _ => {
3606 write!(f, "No interpreter found for {} in {sources}", self.request)
3607 }
3608 }
3609 }
3610}
3611
3612fn disjunction(items: &[&str]) -> String {
3614 match items.len() {
3615 0 => String::new(),
3616 1 => items[0].to_string(),
3617 2 => format!("{} or {}", items[0], items[1]),
3618 _ => {
3619 let last = items.last().unwrap();
3620 format!(
3621 "{}, or {}",
3622 items.iter().take(items.len() - 1).join(", "),
3623 last
3624 )
3625 }
3626 }
3627}
3628
3629fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3630 release
3631 .iter()
3632 .map(|x| match u8::try_from(*x) {
3633 Ok(x) => Ok(x),
3634 Err(e) => Err(e),
3635 })
3636 .collect()
3637}
3638
3639fn split_wheel_tag_release_version(version: Version) -> Version {
3646 let release = version.release();
3647 if release.len() != 1 {
3648 return version;
3649 }
3650
3651 let release = release[0].to_string();
3652 let mut chars = release.chars();
3653 let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3654 return version;
3655 };
3656
3657 let Ok(minor) = chars.as_str().parse::<u32>() else {
3658 return version;
3659 };
3660
3661 version.with_release([u64::from(major), u64::from(minor)])
3662}
3663
3664#[cfg(test)]
3665mod tests {
3666 use std::{path::PathBuf, str::FromStr};
3667
3668 use assert_fs::{TempDir, prelude::*};
3669 use target_lexicon::{Aarch64Architecture, Architecture};
3670 use test_log::test;
3671 use uv_distribution_types::RequiresPython;
3672 use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3673
3674 use crate::{
3675 discovery::{PythonRequest, VersionRequest},
3676 downloads::{ArchRequest, PythonDownloadRequest},
3677 implementation::ImplementationName,
3678 };
3679 use uv_platform::{Arch, Libc, Os};
3680
3681 use super::{
3682 DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonVariant,
3683 };
3684
3685 #[test]
3686 fn interpreter_request_from_str() {
3687 assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3688 assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
3689 assert_eq!(
3690 PythonRequest::parse("3.12"),
3691 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
3692 );
3693 assert_eq!(
3694 PythonRequest::parse(">=3.12"),
3695 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3696 );
3697 assert_eq!(
3698 PythonRequest::parse(">=3.12,<3.13"),
3699 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3700 );
3701 assert_eq!(
3702 PythonRequest::parse(">=3.12,<3.13"),
3703 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3704 );
3705
3706 assert_eq!(
3707 PythonRequest::parse("3.13.0a1"),
3708 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3709 );
3710 assert_eq!(
3711 PythonRequest::parse("3.13.0b5"),
3712 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3713 );
3714 assert_eq!(
3715 PythonRequest::parse("3.13.0rc1"),
3716 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3717 );
3718 assert_eq!(
3719 PythonRequest::parse("3.13.1rc1"),
3720 PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3721 "Pre-release version requests require a patch version of zero"
3722 );
3723 assert_eq!(
3724 PythonRequest::parse("3rc1"),
3725 PythonRequest::ExecutableName("3rc1".to_string()),
3726 "Pre-release version requests require a minor version"
3727 );
3728
3729 assert_eq!(
3730 PythonRequest::parse("cpython"),
3731 PythonRequest::Implementation(ImplementationName::CPython)
3732 );
3733
3734 assert_eq!(
3735 PythonRequest::parse("cpython3.12.2"),
3736 PythonRequest::ImplementationVersion(
3737 ImplementationName::CPython,
3738 VersionRequest::from_str("3.12.2").unwrap(),
3739 )
3740 );
3741
3742 assert_eq!(
3743 PythonRequest::parse("cpython-3.13.2"),
3744 PythonRequest::Key(PythonDownloadRequest {
3745 version: Some(VersionRequest::MajorMinorPatch(
3746 3,
3747 13,
3748 2,
3749 PythonVariant::Default
3750 )),
3751 implementation: Some(ImplementationName::CPython),
3752 arch: None,
3753 os: None,
3754 libc: None,
3755 build: None,
3756 prereleases: None
3757 })
3758 );
3759 assert_eq!(
3760 PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3761 PythonRequest::Key(PythonDownloadRequest {
3762 version: Some(VersionRequest::MajorMinorPatch(
3763 3,
3764 13,
3765 2,
3766 PythonVariant::Default
3767 )),
3768 implementation: Some(ImplementationName::CPython),
3769 arch: Some(ArchRequest::Explicit(Arch::new(
3770 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3771 None
3772 ))),
3773 os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3774 libc: Some(Libc::None),
3775 build: None,
3776 prereleases: None
3777 })
3778 );
3779 assert_eq!(
3780 PythonRequest::parse("any-3.13.2"),
3781 PythonRequest::Key(PythonDownloadRequest {
3782 version: Some(VersionRequest::MajorMinorPatch(
3783 3,
3784 13,
3785 2,
3786 PythonVariant::Default
3787 )),
3788 implementation: None,
3789 arch: None,
3790 os: None,
3791 libc: None,
3792 build: None,
3793 prereleases: None
3794 })
3795 );
3796 assert_eq!(
3797 PythonRequest::parse("any-3.13.2-any-aarch64"),
3798 PythonRequest::Key(PythonDownloadRequest {
3799 version: Some(VersionRequest::MajorMinorPatch(
3800 3,
3801 13,
3802 2,
3803 PythonVariant::Default
3804 )),
3805 implementation: None,
3806 arch: Some(ArchRequest::Explicit(Arch::new(
3807 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3808 None
3809 ))),
3810 os: None,
3811 libc: None,
3812 build: None,
3813 prereleases: None
3814 })
3815 );
3816
3817 assert_eq!(
3818 PythonRequest::parse("pypy"),
3819 PythonRequest::Implementation(ImplementationName::PyPy)
3820 );
3821 assert_eq!(
3822 PythonRequest::parse("pp"),
3823 PythonRequest::Implementation(ImplementationName::PyPy)
3824 );
3825 assert_eq!(
3826 PythonRequest::parse("graalpy"),
3827 PythonRequest::Implementation(ImplementationName::GraalPy)
3828 );
3829 assert_eq!(
3830 PythonRequest::parse("gp"),
3831 PythonRequest::Implementation(ImplementationName::GraalPy)
3832 );
3833 assert_eq!(
3834 PythonRequest::parse("cp"),
3835 PythonRequest::Implementation(ImplementationName::CPython)
3836 );
3837 assert_eq!(
3838 PythonRequest::parse("pypy3.10"),
3839 PythonRequest::ImplementationVersion(
3840 ImplementationName::PyPy,
3841 VersionRequest::from_str("3.10").unwrap(),
3842 )
3843 );
3844 assert_eq!(
3845 PythonRequest::parse("pp310"),
3846 PythonRequest::ImplementationVersion(
3847 ImplementationName::PyPy,
3848 VersionRequest::from_str("3.10").unwrap(),
3849 )
3850 );
3851 assert_eq!(
3852 PythonRequest::parse("graalpy3.10"),
3853 PythonRequest::ImplementationVersion(
3854 ImplementationName::GraalPy,
3855 VersionRequest::from_str("3.10").unwrap(),
3856 )
3857 );
3858 assert_eq!(
3859 PythonRequest::parse("gp310"),
3860 PythonRequest::ImplementationVersion(
3861 ImplementationName::GraalPy,
3862 VersionRequest::from_str("3.10").unwrap(),
3863 )
3864 );
3865 assert_eq!(
3866 PythonRequest::parse("cp38"),
3867 PythonRequest::ImplementationVersion(
3868 ImplementationName::CPython,
3869 VersionRequest::from_str("3.8").unwrap(),
3870 )
3871 );
3872 assert_eq!(
3873 PythonRequest::parse("pypy@3.10"),
3874 PythonRequest::ImplementationVersion(
3875 ImplementationName::PyPy,
3876 VersionRequest::from_str("3.10").unwrap(),
3877 )
3878 );
3879 assert_eq!(
3880 PythonRequest::parse("pypy310"),
3881 PythonRequest::ImplementationVersion(
3882 ImplementationName::PyPy,
3883 VersionRequest::from_str("3.10").unwrap(),
3884 )
3885 );
3886 assert_eq!(
3887 PythonRequest::parse("graalpy@3.10"),
3888 PythonRequest::ImplementationVersion(
3889 ImplementationName::GraalPy,
3890 VersionRequest::from_str("3.10").unwrap(),
3891 )
3892 );
3893 assert_eq!(
3894 PythonRequest::parse("graalpy310"),
3895 PythonRequest::ImplementationVersion(
3896 ImplementationName::GraalPy,
3897 VersionRequest::from_str("3.10").unwrap(),
3898 )
3899 );
3900
3901 let tempdir = TempDir::new().unwrap();
3902 assert_eq!(
3903 PythonRequest::parse(tempdir.path().to_str().unwrap()),
3904 PythonRequest::Directory(tempdir.path().to_path_buf()),
3905 "An existing directory is treated as a directory"
3906 );
3907 assert_eq!(
3908 PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3909 PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3910 "A path that does not exist is treated as a file"
3911 );
3912 tempdir.child("bar").touch().unwrap();
3913 assert_eq!(
3914 PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3915 PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3916 "An existing file is treated as a file"
3917 );
3918 assert_eq!(
3919 PythonRequest::parse("./foo"),
3920 PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
3921 "A string with a file system separator is treated as a file"
3922 );
3923 assert_eq!(
3924 PythonRequest::parse("3.13t"),
3925 PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
3926 );
3927 }
3928
3929 #[test]
3930 fn discovery_sources_prefer_system_orders_search_path_first() {
3931 let preferences = DiscoveryPreferences {
3932 python_preference: PythonPreference::System,
3933 environment_preference: EnvironmentPreference::OnlySystem,
3934 };
3935 let sources = preferences.sources(&PythonRequest::Default);
3936
3937 if cfg!(windows) {
3938 assert_eq!(sources, "search path, registry, or managed installations");
3939 } else {
3940 assert_eq!(sources, "search path or managed installations");
3941 }
3942 }
3943
3944 #[test]
3945 fn discovery_sources_only_system_matches_platform_order() {
3946 let preferences = DiscoveryPreferences {
3947 python_preference: PythonPreference::OnlySystem,
3948 environment_preference: EnvironmentPreference::OnlySystem,
3949 };
3950 let sources = preferences.sources(&PythonRequest::Default);
3951
3952 if cfg!(windows) {
3953 assert_eq!(sources, "search path or registry");
3954 } else {
3955 assert_eq!(sources, "search path");
3956 }
3957 }
3958
3959 #[test]
3960 fn interpreter_request_to_canonical_string() {
3961 assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
3962 assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
3963 assert_eq!(
3964 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
3965 "3.12"
3966 );
3967 assert_eq!(
3968 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3969 .to_canonical_string(),
3970 ">=3.12"
3971 );
3972 assert_eq!(
3973 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3974 .to_canonical_string(),
3975 ">=3.12, <3.13"
3976 );
3977
3978 assert_eq!(
3979 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3980 .to_canonical_string(),
3981 "3.13a1"
3982 );
3983
3984 assert_eq!(
3985 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3986 .to_canonical_string(),
3987 "3.13b5"
3988 );
3989
3990 assert_eq!(
3991 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3992 .to_canonical_string(),
3993 "3.13rc1"
3994 );
3995
3996 assert_eq!(
3997 PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
3998 .to_canonical_string(),
3999 "3.13rc4"
4000 );
4001
4002 assert_eq!(
4003 PythonRequest::Version(VersionRequest::from_str("3.14.5rc1").unwrap())
4004 .to_canonical_string(),
4005 "3.14.5rc1"
4006 );
4007
4008 assert_eq!(
4009 PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
4010 "foo"
4011 );
4012 assert_eq!(
4013 PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
4014 "cpython"
4015 );
4016 assert_eq!(
4017 PythonRequest::ImplementationVersion(
4018 ImplementationName::CPython,
4019 VersionRequest::from_str("3.12.2").unwrap(),
4020 )
4021 .to_canonical_string(),
4022 "cpython@3.12.2"
4023 );
4024 assert_eq!(
4025 PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
4026 "pypy"
4027 );
4028 assert_eq!(
4029 PythonRequest::ImplementationVersion(
4030 ImplementationName::PyPy,
4031 VersionRequest::from_str("3.10").unwrap(),
4032 )
4033 .to_canonical_string(),
4034 "pypy@3.10"
4035 );
4036 assert_eq!(
4037 PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
4038 "graalpy"
4039 );
4040 assert_eq!(
4041 PythonRequest::ImplementationVersion(
4042 ImplementationName::GraalPy,
4043 VersionRequest::from_str("3.10").unwrap(),
4044 )
4045 .to_canonical_string(),
4046 "graalpy@3.10"
4047 );
4048
4049 let tempdir = TempDir::new().unwrap();
4050 assert_eq!(
4051 PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
4052 tempdir.path().to_str().unwrap(),
4053 "An existing directory is treated as a directory"
4054 );
4055 assert_eq!(
4056 PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
4057 tempdir.child("foo").path().to_str().unwrap(),
4058 "A path that does not exist is treated as a file"
4059 );
4060 tempdir.child("bar").touch().unwrap();
4061 assert_eq!(
4062 PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
4063 tempdir.child("bar").path().to_str().unwrap(),
4064 "An existing file is treated as a file"
4065 );
4066 assert_eq!(
4067 PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
4068 "./foo",
4069 "A string with a file system separator is treated as a file"
4070 );
4071 }
4072
4073 #[test]
4074 fn version_request_from_str() {
4075 assert_eq!(
4076 VersionRequest::from_str("3").unwrap(),
4077 VersionRequest::Major(3, PythonVariant::Default)
4078 );
4079 assert_eq!(
4080 VersionRequest::from_str("3.12").unwrap(),
4081 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4082 );
4083 assert_eq!(
4084 VersionRequest::from_str("3.12.1").unwrap(),
4085 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4086 );
4087 assert!(VersionRequest::from_str("1.foo.1").is_err());
4088 assert_eq!(
4089 VersionRequest::from_str("3").unwrap(),
4090 VersionRequest::Major(3, PythonVariant::Default)
4091 );
4092 assert_eq!(
4093 VersionRequest::from_str("38").unwrap(),
4094 VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
4095 );
4096 assert_eq!(
4097 VersionRequest::from_str("312").unwrap(),
4098 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4099 );
4100 assert_eq!(
4101 VersionRequest::from_str("3100").unwrap(),
4102 VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
4103 );
4104 assert_eq!(
4105 VersionRequest::from_str("3.13a1").unwrap(),
4106 VersionRequest::MajorMinorPrerelease(
4107 3,
4108 13,
4109 Prerelease {
4110 kind: PrereleaseKind::Alpha,
4111 number: 1
4112 },
4113 PythonVariant::Default
4114 )
4115 );
4116 assert_eq!(
4117 VersionRequest::from_str("313b1").unwrap(),
4118 VersionRequest::MajorMinorPrerelease(
4119 3,
4120 13,
4121 Prerelease {
4122 kind: PrereleaseKind::Beta,
4123 number: 1
4124 },
4125 PythonVariant::Default
4126 )
4127 );
4128 assert_eq!(
4129 VersionRequest::from_str("3.13.0b2").unwrap(),
4130 VersionRequest::MajorMinorPrerelease(
4131 3,
4132 13,
4133 Prerelease {
4134 kind: PrereleaseKind::Beta,
4135 number: 2
4136 },
4137 PythonVariant::Default
4138 )
4139 );
4140 assert_eq!(
4141 VersionRequest::from_str("3.13.0rc3").unwrap(),
4142 VersionRequest::MajorMinorPrerelease(
4143 3,
4144 13,
4145 Prerelease {
4146 kind: PrereleaseKind::Rc,
4147 number: 3
4148 },
4149 PythonVariant::Default
4150 )
4151 );
4152 assert!(
4153 matches!(
4154 VersionRequest::from_str("3rc1"),
4155 Err(Error::InvalidVersionRequest(_))
4156 ),
4157 "Pre-release version requests require a minor version"
4158 );
4159 assert_eq!(
4160 VersionRequest::from_str("3.14.5rc1").unwrap(),
4161 VersionRequest::MajorMinorPatchPrerelease(
4162 3,
4163 14,
4164 5,
4165 Prerelease {
4166 kind: PrereleaseKind::Rc,
4167 number: 1
4168 },
4169 PythonVariant::Default
4170 ),
4171 "Pre-release version requests with a non-zero patch are allowed (e.g., `3.14.5rc1`)"
4172 );
4173 assert_eq!(
4174 VersionRequest::from_str("3.13.2rc1").unwrap(),
4175 VersionRequest::MajorMinorPatchPrerelease(
4176 3,
4177 13,
4178 2,
4179 Prerelease {
4180 kind: PrereleaseKind::Rc,
4181 number: 1
4182 },
4183 PythonVariant::Default
4184 )
4185 );
4186 assert!(
4187 matches!(
4188 VersionRequest::from_str("3.12-dev"),
4189 Err(Error::InvalidVersionRequest(_))
4190 ),
4191 "Development version segments are not allowed"
4192 );
4193 assert!(
4194 matches!(
4195 VersionRequest::from_str("3.12+local"),
4196 Err(Error::InvalidVersionRequest(_))
4197 ),
4198 "Local version segments are not allowed"
4199 );
4200 assert!(
4201 matches!(
4202 VersionRequest::from_str("3.12.post0"),
4203 Err(Error::InvalidVersionRequest(_))
4204 ),
4205 "Post version segments are not allowed"
4206 );
4207 assert!(
4208 matches!(
4210 VersionRequest::from_str("31000"),
4211 Err(Error::InvalidVersionRequest(_))
4212 )
4213 );
4214 assert_eq!(
4215 VersionRequest::from_str("3t").unwrap(),
4216 VersionRequest::Major(3, PythonVariant::Freethreaded)
4217 );
4218 assert_eq!(
4219 VersionRequest::from_str("313t").unwrap(),
4220 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4221 );
4222 assert_eq!(
4223 VersionRequest::from_str("3.13t").unwrap(),
4224 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4225 );
4226 assert_eq!(
4227 VersionRequest::from_str(">=3.13t").unwrap(),
4228 VersionRequest::Range(
4229 VersionSpecifiers::from_str(">=3.13").unwrap(),
4230 PythonVariant::Freethreaded
4231 )
4232 );
4233 assert_eq!(
4234 VersionRequest::from_str(">=3.13").unwrap(),
4235 VersionRequest::Range(
4236 VersionSpecifiers::from_str(">=3.13").unwrap(),
4237 PythonVariant::Default
4238 )
4239 );
4240 assert_eq!(
4241 VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4242 VersionRequest::Range(
4243 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4244 PythonVariant::Freethreaded
4245 )
4246 );
4247 assert!(matches!(
4248 VersionRequest::from_str("3.13tt"),
4249 Err(Error::InvalidVersionRequest(_))
4250 ));
4251
4252 assert_eq!(
4254 VersionRequest::from_str("==3.12").unwrap(),
4255 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4256 );
4257 assert_eq!(
4258 VersionRequest::from_str("==3.12.1").unwrap(),
4259 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4260 );
4261 }
4262
4263 #[test]
4264 fn version_request_from_specifiers() {
4265 assert_eq!(
4267 VersionRequest::from_specifiers(
4268 VersionSpecifiers::from_str("==3.12").unwrap(),
4269 PythonVariant::Default
4270 ),
4271 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4272 );
4273 assert_eq!(
4274 VersionRequest::from_specifiers(
4275 VersionSpecifiers::from_str("==3.12.1").unwrap(),
4276 PythonVariant::Default
4277 ),
4278 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4279 );
4280
4281 assert_eq!(
4283 VersionRequest::from_specifiers(
4284 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4285 PythonVariant::Default
4286 ),
4287 VersionRequest::Range(
4288 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4289 PythonVariant::Default
4290 )
4291 );
4292
4293 assert_eq!(
4295 VersionRequest::from_specifiers(
4296 VersionSpecifiers::from_str(">=3.12").unwrap(),
4297 PythonVariant::Default
4298 ),
4299 VersionRequest::Range(
4300 VersionSpecifiers::from_str(">=3.12").unwrap(),
4301 PythonVariant::Default
4302 )
4303 );
4304
4305 assert_eq!(
4307 VersionRequest::from_specifiers(
4308 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4309 PythonVariant::Default
4310 ),
4311 VersionRequest::Range(
4312 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4313 PythonVariant::Default
4314 )
4315 );
4316 }
4317
4318 #[test]
4319 fn executable_names_from_request() {
4320 fn case(request: &str, expected: &[&str]) {
4321 let (implementation, version) = match PythonRequest::parse(request) {
4322 PythonRequest::Any => (None, VersionRequest::Any),
4323 PythonRequest::Default => (None, VersionRequest::Default),
4324 PythonRequest::Version(version) => (None, version),
4325 PythonRequest::ImplementationVersion(implementation, version) => {
4326 (Some(implementation), version)
4327 }
4328 PythonRequest::Implementation(implementation) => {
4329 (Some(implementation), VersionRequest::Default)
4330 }
4331 result => {
4332 panic!("Test cases should request versions or implementations; got {result:?}")
4333 }
4334 };
4335
4336 let result: Vec<_> = version
4337 .executable_names(implementation.as_ref())
4338 .into_iter()
4339 .map(|name| name.to_string())
4340 .collect();
4341
4342 let expected: Vec<_> = expected
4343 .iter()
4344 .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4345 .collect();
4346
4347 assert_eq!(result, expected, "mismatch for case \"{request}\"");
4348 }
4349
4350 case(
4351 "any",
4352 &[
4353 "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4354 "pyodide", "pyodide3",
4355 ],
4356 );
4357
4358 case("default", &["python", "python3"]);
4359
4360 case("3", &["python3", "python"]);
4361
4362 case("4", &["python4", "python"]);
4363
4364 case("3.13", &["python3.13", "python3", "python"]);
4365
4366 case("pypy", &["pypy", "pypy3", "python", "python3"]);
4367
4368 case(
4369 "pypy@3.10",
4370 &[
4371 "pypy3.10",
4372 "pypy3",
4373 "pypy",
4374 "python3.10",
4375 "python3",
4376 "python",
4377 ],
4378 );
4379
4380 case(
4381 "3.13t",
4382 &[
4383 "python3.13t",
4384 "python3.13",
4385 "python3t",
4386 "python3",
4387 "pythont",
4388 "python",
4389 ],
4390 );
4391 case("3t", &["python3t", "python3", "pythont", "python"]);
4392
4393 case(
4394 "3.13.2",
4395 &["python3.13.2", "python3.13", "python3", "python"],
4396 );
4397
4398 case(
4399 "3.13rc2",
4400 &["python3.13rc2", "python3.13", "python3", "python"],
4401 );
4402 }
4403
4404 #[test]
4405 fn test_try_split_prefix_and_version() {
4406 assert!(matches!(
4407 PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4408 Ok(None),
4409 ));
4410 assert!(matches!(
4411 PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4412 Ok(Some(_)),
4413 ));
4414 assert!(matches!(
4415 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4416 Ok(Some(_)),
4417 ));
4418 assert!(matches!(
4419 PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4420 Ok(None),
4421 ));
4422 assert!(
4424 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4425 );
4426 assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4428 }
4429
4430 #[test]
4431 fn version_request_as_pep440_version() {
4432 assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4434 assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4435 assert_eq!(
4436 VersionRequest::from_str(">=3.10")
4437 .unwrap()
4438 .as_pep440_version(),
4439 None
4440 );
4441
4442 assert_eq!(
4444 VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4445 Some(Version::from_str("3").unwrap())
4446 );
4447
4448 assert_eq!(
4450 VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4451 Some(Version::from_str("3.12").unwrap())
4452 );
4453
4454 assert_eq!(
4456 VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4457 Some(Version::from_str("3.12.5").unwrap())
4458 );
4459
4460 assert_eq!(
4462 VersionRequest::MajorMinorPrerelease(
4463 3,
4464 14,
4465 Prerelease {
4466 kind: PrereleaseKind::Alpha,
4467 number: 1
4468 },
4469 PythonVariant::Default
4470 )
4471 .as_pep440_version(),
4472 Some(Version::from_str("3.14.0a1").unwrap())
4473 );
4474 assert_eq!(
4475 VersionRequest::MajorMinorPrerelease(
4476 3,
4477 14,
4478 Prerelease {
4479 kind: PrereleaseKind::Beta,
4480 number: 2
4481 },
4482 PythonVariant::Default
4483 )
4484 .as_pep440_version(),
4485 Some(Version::from_str("3.14.0b2").unwrap())
4486 );
4487 assert_eq!(
4488 VersionRequest::MajorMinorPrerelease(
4489 3,
4490 13,
4491 Prerelease {
4492 kind: PrereleaseKind::Rc,
4493 number: 3
4494 },
4495 PythonVariant::Default
4496 )
4497 .as_pep440_version(),
4498 Some(Version::from_str("3.13.0rc3").unwrap())
4499 );
4500
4501 assert_eq!(
4503 VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4504 Some(Version::from_str("3").unwrap())
4505 );
4506 assert_eq!(
4507 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4508 Some(Version::from_str("3.13").unwrap())
4509 );
4510 }
4511
4512 #[test]
4513 fn python_request_as_pep440_version() {
4514 assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4516 assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4517
4518 assert_eq!(
4520 PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4521 .as_pep440_version(),
4522 Some(Version::from_str("3.11").unwrap())
4523 );
4524
4525 assert_eq!(
4527 PythonRequest::ImplementationVersion(
4528 ImplementationName::CPython,
4529 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4530 )
4531 .as_pep440_version(),
4532 Some(Version::from_str("3.12.1").unwrap())
4533 );
4534
4535 assert_eq!(
4537 PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4538 None
4539 );
4540
4541 assert_eq!(
4543 PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4544 Some(Version::from_str("3.13.2").unwrap())
4545 );
4546
4547 assert_eq!(
4549 PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4550 None
4551 );
4552
4553 assert_eq!(
4555 PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4556 None
4557 );
4558 }
4559
4560 #[test]
4561 fn intersects_requires_python_exact() {
4562 let requires_python =
4563 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4564
4565 assert!(PythonRequest::parse("3.12").intersects_requires_python(&requires_python));
4566 assert!(!PythonRequest::parse("3.11").intersects_requires_python(&requires_python));
4567 }
4568
4569 #[test]
4570 fn intersects_requires_python_major() {
4571 let requires_python =
4572 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4573
4574 assert!(PythonRequest::parse("3").intersects_requires_python(&requires_python));
4576 assert!(!PythonRequest::parse("2").intersects_requires_python(&requires_python));
4578 }
4579
4580 #[test]
4581 fn intersects_requires_python_range() {
4582 let requires_python =
4583 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4584
4585 assert!(PythonRequest::parse(">=3.12,<3.13").intersects_requires_python(&requires_python));
4586 assert!(!PythonRequest::parse(">=3.10,<3.12").intersects_requires_python(&requires_python));
4587 }
4588
4589 #[test]
4590 fn intersects_requires_python_implementation_range() {
4591 let requires_python =
4592 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4593
4594 assert!(
4595 PythonRequest::parse("cpython@>=3.12,<3.13")
4596 .intersects_requires_python(&requires_python)
4597 );
4598 assert!(
4599 !PythonRequest::parse("cpython@>=3.10,<3.12")
4600 .intersects_requires_python(&requires_python)
4601 );
4602 }
4603
4604 #[test]
4605 fn intersects_requires_python_no_version() {
4606 let requires_python =
4607 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4608
4609 assert!(PythonRequest::Any.intersects_requires_python(&requires_python));
4611 assert!(PythonRequest::Default.intersects_requires_python(&requires_python));
4612 assert!(
4613 PythonRequest::Implementation(ImplementationName::CPython)
4614 .intersects_requires_python(&requires_python)
4615 );
4616 }
4617}