1use itertools::{Either, Itertools};
2use rayon::iter::{IntoParallelIterator, ParallelIterator};
3use regex::Regex;
4use rustc_hash::{FxBuildHasher, FxHashSet};
5use same_file::is_same_file;
6use std::borrow::Cow;
7use std::cmp::Reverse;
8use std::env::consts::EXE_SUFFIX;
9use std::fmt::{self, Debug, Formatter};
10use std::sync::atomic::Ordering;
11use std::{env, io, iter};
12use std::{path::Path, path::PathBuf, str::FromStr};
13use thiserror::Error;
14use tracing::{debug, instrument, trace};
15use uv_cache::Cache;
16use uv_client::BaseClientBuilder;
17use uv_distribution_types::RequiresPython;
18use uv_errors::Hints;
19use uv_fs::Simplified;
20use uv_fs::which::is_executable;
21use uv_pep440::{
22 LowerBound, Prerelease, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
23 release_specifiers_to_ranges,
24};
25use uv_static::EnvVars;
26use uv_warnings::{warn_user_once, write_warning_chain};
27use which::{which, which_all};
28
29use crate::downloads::{ManagedPythonDownloadList, PlatformRequest, PythonDownloadRequest};
30use crate::implementation::ImplementationName;
31use crate::installation::{PythonInstallation, PythonInstallationKey};
32use crate::interpreter::Error as InterpreterError;
33use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
34use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
35#[cfg(windows)]
36use crate::microsoft_store::find_microsoft_store_pythons;
37use crate::python_version::python_build_versions_from_env;
38use crate::virtualenv::Error as VirtualEnvError;
39use crate::virtualenv::{
40 CondaEnvironmentKind, conda_environment_from_env, virtualenv_from_env,
41 virtualenv_from_working_dir, virtualenv_python_executable,
42};
43#[cfg(windows)]
44use crate::windows_registry::{WindowsPython, registry_pythons};
45use crate::{BrokenLink, Interpreter, PythonVersion};
46
47#[derive(Debug, Clone, Eq, Default)]
51pub enum PythonRequest {
52 #[default]
57 Default,
58 Any,
60 Version(VersionRequest),
62 Directory(PathBuf),
64 File(PathBuf),
66 ExecutableName(String),
68 Implementation(ImplementationName),
70 ImplementationVersion(ImplementationName, VersionRequest),
72 Key(PythonDownloadRequest),
75}
76
77impl PartialEq for PythonRequest {
78 fn eq(&self, other: &Self) -> bool {
79 self.to_canonical_string() == other.to_canonical_string()
80 }
81}
82
83impl std::hash::Hash for PythonRequest {
84 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
85 self.to_canonical_string().hash(state);
86 }
87}
88
89impl<'a> serde::Deserialize<'a> for PythonRequest {
90 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91 where
92 D: serde::Deserializer<'a>,
93 {
94 let s = <Cow<'_, str>>::deserialize(deserializer)?;
95 Ok(Self::parse(&s))
96 }
97}
98
99impl serde::Serialize for PythonRequest {
100 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
101 where
102 S: serde::Serializer,
103 {
104 let s = self.to_canonical_string();
105 serializer.serialize_str(&s)
106 }
107}
108
109#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
110#[serde(deny_unknown_fields, rename_all = "kebab-case")]
111#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113pub enum PythonPreference {
114 OnlyManaged,
116 #[default]
117 Managed,
122 System,
126 OnlySystem,
128}
129
130#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
131#[serde(deny_unknown_fields, rename_all = "kebab-case")]
132#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
133#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
134pub enum PythonDownloads {
135 #[default]
137 #[serde(alias = "auto")]
138 Automatic,
139 Manual,
141 Never,
143}
144
145impl FromStr for PythonDownloads {
146 type Err = String;
147
148 fn from_str(s: &str) -> Result<Self, Self::Err> {
149 match s.to_ascii_lowercase().as_str() {
150 "auto" | "automatic" | "true" | "1" => Ok(Self::Automatic),
151 "manual" => Ok(Self::Manual),
152 "never" | "false" | "0" => Ok(Self::Never),
153 _ => Err(format!("Invalid value for `python-download`: '{s}'")),
154 }
155 }
156}
157
158impl From<bool> for PythonDownloads {
159 fn from(value: bool) -> Self {
160 if value { Self::Automatic } else { Self::Never }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
165pub enum EnvironmentPreference {
166 #[default]
168 OnlyVirtual,
169 ExplicitSystem,
171 OnlySystem,
173 Any,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Default)]
178pub(crate) struct DiscoveryPreferences {
179 python_preference: PythonPreference,
180 environment_preference: EnvironmentPreference,
181}
182
183#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
184pub enum PythonVariant {
185 #[default]
186 Default,
187 Debug,
188 Freethreaded,
189 FreethreadedDebug,
190 Gil,
191 GilDebug,
192}
193
194#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
196pub enum VersionRequest {
197 #[default]
199 Default,
200 Any,
202 Major(u8, PythonVariant),
203 MajorMinor(u8, u8, PythonVariant),
204 MajorMinorPatch(u8, u8, u8, PythonVariant),
205 MajorMinorPrerelease(u8, u8, Prerelease, PythonVariant),
206 MajorMinorPatchPrerelease(u8, u8, u8, Prerelease, PythonVariant),
207 Range(VersionSpecifiers, PythonVariant),
208}
209
210type FindPythonResult = Result<PythonInstallation, PythonNotFound>;
214
215#[derive(Clone, Debug, Error)]
219pub struct PythonNotFound {
220 pub(super) request: PythonRequest,
221 pub(super) python_preference: PythonPreference,
222 pub(super) environment_preference: EnvironmentPreference,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)]
227pub enum PythonSource {
228 ProvidedPath,
230 ActiveEnvironment,
232 CondaPrefix,
234 BaseCondaPrefix,
236 DiscoveredEnvironment,
238 SearchPath,
240 SearchPathFirst,
242 Registry,
244 MicrosoftStore,
246 Managed,
248 ParentInterpreter,
250}
251
252struct PythonExecutableGroup(Vec<(PythonSource, PathBuf)>);
257
258impl PythonExecutableGroup {
259 fn new(executables: Vec<(PythonSource, PathBuf)>) -> Option<Self> {
260 (!executables.is_empty()).then_some(Self(executables))
261 }
262
263 fn filter(mut self, mut predicate: impl FnMut(PythonSource, &Path) -> bool) -> Option<Self> {
264 self.0.retain(|(source, path)| predicate(*source, path));
265 (!self.0.is_empty()).then_some(self)
266 }
267}
268
269#[derive(Error, Debug)]
270pub enum Error {
271 #[error(transparent)]
272 Io(#[from] io::Error),
273
274 #[error("Failed to inspect Python interpreter from {} at `{}` ", _2, _1.user_display())]
276 Query(
277 #[source] Box<crate::interpreter::Error>,
278 PathBuf,
279 PythonSource,
280 ),
281
282 #[error("Failed to discover managed Python installations")]
285 ManagedPython(#[from] crate::managed::Error),
286
287 #[error(transparent)]
289 VirtualEnv(#[from] crate::virtualenv::Error),
290
291 #[cfg(windows)]
292 #[error("Failed to query installed Python versions from the Windows registry")]
293 RegistryError(#[from] windows::core::Error),
294
295 #[error(transparent)]
296 InvalidEnvironmentVariable(#[from] uv_static::InvalidEnvironmentVariable),
297
298 #[error("Invalid version request: {0}")]
300 InvalidVersionRequest(String),
301
302 #[error("Requesting the 'latest' Python version is not yet supported")]
304 LatestVersionRequest,
305
306 #[error("Interpreter discovery for `{0}` requires `{1}` but only `{2}` is allowed")]
308 SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
309
310 #[error(transparent)]
311 BuildVersion(#[from] crate::python_version::BuildVersionError),
312}
313
314impl uv_errors::Hint for Error {
315 fn hints(&self) -> uv_errors::Hints<'_> {
316 match self {
317 Self::Query(err, _, _) => err.hints(),
318 _ => uv_errors::Hints::none(),
319 }
320 }
321}
322
323fn python_executables_from_virtual_environments<'a>()
332-> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a {
333 let from_active_environment = iter::once_with(|| {
334 virtualenv_from_env()
335 .into_iter()
336 .map(virtualenv_python_executable)
337 .map(|path| Ok((PythonSource::ActiveEnvironment, path)))
338 })
339 .flatten();
340
341 let from_conda_environment = iter::once_with(move || {
343 conda_environment_from_env(CondaEnvironmentKind::Child)
344 .into_iter()
345 .map(virtualenv_python_executable)
346 .map(|path| Ok((PythonSource::CondaPrefix, path)))
347 })
348 .flatten();
349
350 let from_discovered_environment = iter::once_with(|| {
351 virtualenv_from_working_dir()
352 .map(|path| {
353 path.map(virtualenv_python_executable)
354 .map(|path| (PythonSource::DiscoveredEnvironment, path))
355 .into_iter()
356 })
357 .map_err(Error::from)
358 })
359 .flatten_ok();
360
361 from_active_environment
362 .chain(from_conda_environment)
363 .chain(from_discovered_environment)
364}
365
366fn python_executables_from_installed<'a>(
385 version: &'a VersionRequest,
386 implementation: Option<&'a ImplementationName>,
387 platform: PlatformRequest,
388 preference: PythonPreference,
389) -> Box<dyn Iterator<Item = Result<PythonExecutableGroup, Error>> + 'a> {
390 let from_managed_installations = iter::once_with(move || {
391 ManagedPythonInstallations::from_settings(None)
392 .map_err(Error::from)
393 .and_then(|installed_installations| {
394 debug!(
395 "Searching for managed installations at `{}`",
396 installed_installations.root().user_display()
397 );
398 let installations = ManagedPythonInstallations::find_matching_current_platform()?;
399
400 let build_versions = python_build_versions_from_env()?;
401
402 Ok(installations
405 .into_iter()
406 .filter(move |installation| {
407 if !version.matches_version(&installation.version()) {
408 debug!("Skipping managed installation `{installation}`: does not satisfy `{version}`");
409 return false;
410 }
411 if !platform.matches(installation.platform()) {
412 debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
413 return false;
414 }
415
416 if let Some(requested_build) = build_versions.get(&installation.implementation()) {
417 let Some(installation_build) = installation.build() else {
418 debug!(
419 "Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation"
420 );
421 return false;
422 };
423 if installation_build != requested_build {
424 debug!(
425 "Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`"
426 );
427 return false;
428 }
429 }
430
431 true
432 })
433 .inspect(|installation| debug!("Found managed installation `{installation}`"))
434 .map(move |installation| {
435 let executable = version
438 .patch()
439 .is_none()
440 .then(|| {
441 PythonMinorVersionLink::from_installation(
442 &installation,
443 )
444 .filter(PythonMinorVersionLink::exists)
445 .map(
446 |minor_version_link| {
447 minor_version_link.symlink_executable.clone()
448 },
449 )
450 })
451 .flatten()
452 .unwrap_or_else(|| installation.executable(false));
453 (PythonSource::Managed, executable)
454 })
455 )
456 })
457 })
458 .flatten_ok()
459 .map_ok(|executable| PythonExecutableGroup(vec![executable]));
460
461 let from_search_path = iter::once_with(move || {
462 let mut first = true;
463 python_executables_from_search_path(version, implementation).filter_map(move |paths| {
464 let executables = paths
465 .into_iter()
466 .map(|path| {
467 let source = if first {
468 first = false;
469 PythonSource::SearchPathFirst
470 } else {
471 PythonSource::SearchPath
472 };
473 (source, path)
474 })
475 .collect();
476 PythonExecutableGroup::new(executables).map(Ok)
477 })
478 })
479 .flatten();
480
481 #[cfg(windows)]
482 let from_windows_registry: Box<
483 dyn Iterator<Item = Result<PythonExecutableGroup, Error>> + 'a,
484 > = match uv_static::parse_boolish_environment_variable(EnvVars::UV_PYTHON_NO_REGISTRY) {
485 Ok(Some(true)) => Box::new(iter::empty()),
486 Ok(Some(false) | None) => Box::new(
487 iter::once_with(move || {
488 let version_filter = move |entry: &WindowsPython| {
490 if let Some(found) = &entry.version {
491 if found.string.chars().filter(|c| *c == '.').count() == 1 {
493 version.matches_major_minor(found.major(), found.minor())
494 } else {
495 version.matches_version(found)
496 }
497 } else {
498 true
499 }
500 };
501
502 registry_pythons()
503 .map(|entries| {
504 entries
505 .into_iter()
506 .filter(version_filter)
507 .map(|entry| (PythonSource::Registry, entry.path))
508 .chain(
509 find_microsoft_store_pythons()
510 .filter(version_filter)
511 .map(|entry| (PythonSource::MicrosoftStore, entry.path)),
512 )
513 })
514 .map_err(Error::from)
515 })
516 .flatten_ok()
517 .map_ok(|executable| PythonExecutableGroup(vec![executable])),
518 ),
519 Err(err) => Box::new(iter::once(Err(Error::from(err)))),
520 };
521
522 #[cfg(not(windows))]
523 let from_windows_registry: Box<
524 dyn Iterator<Item = Result<PythonExecutableGroup, Error>> + 'a,
525 > = Box::new(iter::empty());
526
527 match preference {
528 PythonPreference::OnlyManaged => {
529 if std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED).is_ok() {
533 Box::new(from_managed_installations.chain(from_search_path))
534 } else {
535 Box::new(from_managed_installations)
536 }
537 }
538 PythonPreference::Managed => Box::new(
539 from_managed_installations
540 .chain(from_search_path)
541 .chain(from_windows_registry),
542 ),
543 PythonPreference::System => Box::new(
544 from_search_path
545 .chain(from_windows_registry)
546 .chain(from_managed_installations),
547 ),
548 PythonPreference::OnlySystem => Box::new(from_search_path.chain(from_windows_registry)),
549 }
550}
551
552fn python_executables<'a>(
562 version: &'a VersionRequest,
563 implementation: Option<&'a ImplementationName>,
564 platform: PlatformRequest,
565 environments: EnvironmentPreference,
566 preference: PythonPreference,
567) -> Box<dyn Iterator<Item = Result<PythonExecutableGroup, Error>> + 'a> {
568 let from_parent_interpreter = iter::once_with(|| {
570 env::var_os(EnvVars::UV_INTERNAL__PARENT_INTERPRETER)
571 .into_iter()
572 .map(|path| {
573 Ok(PythonExecutableGroup(vec![(
574 PythonSource::ParentInterpreter,
575 PathBuf::from(path),
576 )]))
577 })
578 })
579 .flatten();
580
581 let from_base_conda_environment = iter::once_with(move || {
583 conda_environment_from_env(CondaEnvironmentKind::Base)
584 .into_iter()
585 .map(virtualenv_python_executable)
586 .map(|path| {
587 Ok(PythonExecutableGroup(vec![(
588 PythonSource::BaseCondaPrefix,
589 path,
590 )]))
591 })
592 })
593 .flatten();
594
595 let from_virtual_environments = python_executables_from_virtual_environments()
596 .map_ok(|executable| PythonExecutableGroup(vec![executable]));
597 let from_installed =
598 python_executables_from_installed(version, implementation, platform, preference);
599
600 match environments {
604 EnvironmentPreference::OnlyVirtual => {
605 Box::new(from_parent_interpreter.chain(from_virtual_environments))
606 }
607 EnvironmentPreference::ExplicitSystem | EnvironmentPreference::Any => Box::new(
608 from_parent_interpreter
609 .chain(from_virtual_environments)
610 .chain(from_base_conda_environment)
611 .chain(from_installed),
612 ),
613 EnvironmentPreference::OnlySystem => Box::new(
614 from_parent_interpreter
615 .chain(from_base_conda_environment)
616 .chain(from_installed),
617 ),
618 }
619}
620
621fn python_executables_from_search_path<'a>(
649 version: &'a VersionRequest,
650 implementation: Option<&'a ImplementationName>,
651) -> impl Iterator<Item = Vec<PathBuf>> + 'a {
652 let search_path = env::var_os(EnvVars::UV_PYTHON_SEARCH_PATH)
654 .unwrap_or(env::var_os(EnvVars::PATH).unwrap_or_default());
655
656 let possible_names: Vec<_> = version
657 .executable_names(implementation)
658 .into_iter()
659 .map(|name| name.to_string())
660 .collect();
661
662 trace!(
663 "Searching PATH for executables: {}",
664 possible_names.join(", ")
665 );
666
667 let search_dirs: Vec<_> = env::split_paths(&search_path).collect();
671 let mut seen_dirs = FxHashSet::with_capacity_and_hasher(search_dirs.len(), FxBuildHasher);
672 search_dirs
673 .into_iter()
674 .filter(|dir| dir.is_dir())
675 .flat_map(move |dir| {
676 let dir_clone = dir.clone();
678 trace!(
679 "Checking `PATH` directory for interpreters: {}",
680 dir.display()
681 );
682 same_file::Handle::from_path(&dir)
683 .map(|handle| seen_dirs.insert(handle))
686 .inspect(|fresh_dir| {
687 if !fresh_dir {
688 trace!("Skipping already seen directory: {}", dir.display());
689 }
690 })
691 .unwrap_or(true)
693 .then(|| {
694 let minor_version_directory = dir_clone.clone();
695
696 possible_names
697 .clone()
698 .into_iter()
699 .flat_map(move |name| {
700 which::which_in_global(&*name, Some(&dir))
702 .into_iter()
703 .flatten()
704 .filter(|path| !is_windows_store_shim(path))
705 .map(|path| vec![path])
706 .collect::<Vec<_>>()
709 })
710 .chain(
711 iter::once_with(move || {
712 find_all_minor(implementation, version, &minor_version_directory)
713 .filter(|path| !is_windows_store_shim(path))
714 .collect::<Vec<_>>()
715 })
716 .filter(|paths| !paths.is_empty()),
717 )
718 .inspect(|paths| {
719 for path in paths {
720 trace!("Found possible Python executable: {}", path.display());
721 }
722 })
723 .chain(
724 cfg!(windows)
726 .then(move || {
727 which::which_in_global("python.bat", Some(&dir_clone))
728 .into_iter()
729 .flatten()
730 .map(|path| vec![path])
731 .collect::<Vec<_>>()
732 })
733 .into_iter()
734 .flatten(),
735 )
736 })
737 .into_iter()
738 .flatten()
739 })
740}
741
742fn find_all_minor(
747 implementation: Option<&ImplementationName>,
748 version_request: &VersionRequest,
749 dir: &Path,
750) -> impl Iterator<Item = PathBuf> + use<> {
751 match version_request {
752 &VersionRequest::Any
753 | VersionRequest::Default
754 | VersionRequest::Major(_, _)
755 | VersionRequest::Range(_, _) => {
756 let regex = if let Some(implementation) = implementation {
757 Regex::new(&format!(
758 r"^({}|python3)\.(?<minor>\d\d?)t?{}$",
759 regex::escape(&implementation.to_string()),
760 regex::escape(EXE_SUFFIX)
761 ))
762 .unwrap()
763 } else {
764 Regex::new(&format!(
765 r"^python3\.(?<minor>\d\d?)t?{}$",
766 regex::escape(EXE_SUFFIX)
767 ))
768 .unwrap()
769 };
770 let all_minors = fs_err::read_dir(dir)
771 .into_iter()
772 .flatten()
773 .flatten()
774 .map(|entry| entry.path())
775 .filter(move |path| {
776 let Some(filename) = path.file_name() else {
777 return false;
778 };
779 let Some(filename) = filename.to_str() else {
780 return false;
781 };
782 let Some(captures) = regex.captures(filename) else {
783 return false;
784 };
785
786 let minor = captures["minor"].parse().ok();
788 if let Some(minor) = minor {
789 if minor < 6 {
791 return false;
792 }
793 if !version_request.matches_major_minor(3, minor) {
795 return false;
796 }
797 }
798 true
799 })
800 .filter(|path| is_executable(path))
801 .collect::<Vec<_>>();
802 Either::Left(all_minors.into_iter())
803 }
804 VersionRequest::MajorMinor(_, _, _)
805 | VersionRequest::MajorMinorPatch(_, _, _, _)
806 | VersionRequest::MajorMinorPrerelease(_, _, _, _)
807 | VersionRequest::MajorMinorPatchPrerelease(_, _, _, _, _) => Either::Right(iter::empty()),
808 }
809}
810
811#[derive(Debug, Clone, Copy)]
813enum QueryStrategy {
814 Sequential,
816 Parallel,
818}
819
820fn python_installations<'a>(
830 version: &'a VersionRequest,
831 implementation: Option<&'a ImplementationName>,
832 platform: PlatformRequest,
833 environments: EnvironmentPreference,
834 preference: PythonPreference,
835 cache: &'a Cache,
836 strategy: QueryStrategy,
837) -> Box<dyn Iterator<Item = Result<PythonInstallation, Error>> + 'a> {
838 Box::new(
839 python_installations_from_executables(
840 python_executables(version, implementation, platform, environments, preference)
844 .filter_map(move |result| match result {
845 Ok(group) => group
846 .filter(|source, path| {
847 source_satisfies_environment_preference(source, path, environments)
848 })
849 .map(Ok),
850 Err(error) => Some(Err(error)),
851 }),
852 cache,
853 strategy,
854 )
855 .filter_ok(move |installation| {
856 installation.satisfies_preferences(version, environments, preference)
857 })
858 .map_ok(PythonInstallation::maybe_with_test_source),
859 )
860}
861
862fn python_installation_from_executable(
864 source: PythonSource,
865 path: PathBuf,
866 cache: &Cache,
867) -> Result<PythonInstallation, Error> {
868 Interpreter::query(&path, cache)
869 .map(|interpreter| PythonInstallation {
870 source,
871 interpreter,
872 })
873 .inspect(|installation| {
874 debug!(
875 "Found `{}` at `{}` ({source})",
876 installation.key(),
877 path.display()
878 );
879 })
880 .map_err(|err| Error::Query(Box::new(err), path, source))
881 .inspect_err(|err| debug!("{err}"))
882}
883
884fn python_installations_from_executables<'a>(
886 executables: impl Iterator<Item = Result<PythonExecutableGroup, Error>> + 'a,
887 cache: &'a Cache,
888 strategy: QueryStrategy,
889) -> Box<dyn Iterator<Item = Result<PythonInstallation, Error>> + 'a> {
890 match strategy {
891 QueryStrategy::Sequential => Box::new(executables.flat_map(move |group| {
892 python_installations_from_executable_group(group, cache, strategy)
893 })),
894 QueryStrategy::Parallel => {
895 let items: Vec<Result<PythonExecutableGroup, Error>> = executables.collect();
896 let results: Vec<Vec<Result<PythonInstallation, Error>>> = items
897 .into_par_iter()
898 .map(|group| {
899 python_installations_from_executable_group(group, cache, strategy)
900 .collect::<Vec<_>>()
901 })
902 .collect();
903 Box::new(results.into_iter().flatten())
904 }
905 }
906}
907
908fn python_installations_from_executable_group(
910 group: Result<PythonExecutableGroup, Error>,
911 cache: &Cache,
912 strategy: QueryStrategy,
913) -> impl Iterator<Item = Result<PythonInstallation, Error>> + use<> {
914 match group {
915 Err(error) => Either::Left(iter::once(Err(error))),
916 Ok(PythonExecutableGroup(executables)) => {
917 let mut installations = match strategy {
918 QueryStrategy::Sequential => executables
919 .into_iter()
920 .map(|(source, path)| python_installation_from_executable(source, path, cache))
921 .collect::<Vec<_>>(),
922 QueryStrategy::Parallel => executables
923 .into_par_iter()
924 .map(|(source, path)| python_installation_from_executable(source, path, cache))
925 .collect::<Vec<_>>(),
926 };
927
928 sort_installations_by_key(&mut installations, PythonInstallation::key);
929
930 Either::Right(installations.into_iter())
931 }
932 }
933}
934
935fn sort_installations_by_key<T, K: Ord>(
937 installations: &mut [Result<T, Error>],
938 key: impl Fn(&T) -> K,
939) {
940 for candidates in
943 installations.split_mut(|result| result.as_ref().is_err_and(Error::is_critical))
944 {
945 candidates.sort_by_key(|result| Reverse(result.as_ref().ok().map(&key)));
946 }
947}
948
949fn interpreter_satisfies_environment_preference(
956 source: PythonSource,
957 interpreter: &Interpreter,
958 preference: EnvironmentPreference,
959) -> bool {
960 match (
961 preference,
962 interpreter.is_virtualenv() || (matches!(source, PythonSource::CondaPrefix)),
964 ) {
965 (EnvironmentPreference::Any, _) => true,
966 (EnvironmentPreference::OnlyVirtual, true) => true,
967 (EnvironmentPreference::OnlyVirtual, false) => {
968 debug!(
969 "Ignoring Python interpreter at `{}`: only virtual environments allowed",
970 interpreter.sys_executable().display()
971 );
972 false
973 }
974 (EnvironmentPreference::ExplicitSystem, true) => true,
975 (EnvironmentPreference::ExplicitSystem, false) => {
976 if matches!(
977 source,
978 PythonSource::ProvidedPath | PythonSource::ParentInterpreter
979 ) {
980 debug!(
981 "Allowing explicitly requested system Python interpreter at `{}`",
982 interpreter.sys_executable().display()
983 );
984 true
985 } else {
986 debug!(
987 "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
988 interpreter.sys_executable().display()
989 );
990 false
991 }
992 }
993 (EnvironmentPreference::OnlySystem, true) => {
994 debug!(
995 "Ignoring Python interpreter at `{}`: system interpreter required",
996 interpreter.sys_executable().display()
997 );
998 false
999 }
1000 (EnvironmentPreference::OnlySystem, false) => true,
1001 }
1002}
1003
1004fn source_satisfies_environment_preference(
1011 source: PythonSource,
1012 interpreter_path: &Path,
1013 preference: EnvironmentPreference,
1014) -> bool {
1015 match preference {
1016 EnvironmentPreference::Any => true,
1017 EnvironmentPreference::OnlyVirtual => {
1018 if source.is_maybe_virtualenv() {
1019 true
1020 } else {
1021 debug!(
1022 "Ignoring Python interpreter at `{}`: only virtual environments allowed",
1023 interpreter_path.display()
1024 );
1025 false
1026 }
1027 }
1028 EnvironmentPreference::ExplicitSystem => {
1029 if source.is_maybe_virtualenv() {
1030 true
1031 } else {
1032 debug!(
1033 "Ignoring Python interpreter at `{}`: system interpreter not explicitly requested",
1034 interpreter_path.display()
1035 );
1036 false
1037 }
1038 }
1039 EnvironmentPreference::OnlySystem => {
1040 if source.is_maybe_system() {
1041 true
1042 } else {
1043 debug!(
1044 "Ignoring Python interpreter at `{}`: system interpreter required",
1045 interpreter_path.display()
1046 );
1047 false
1048 }
1049 }
1050 }
1051}
1052
1053impl Error {
1057 pub(crate) fn is_critical(&self) -> bool {
1058 match self {
1059 Self::Query(err, _, source) => match &**err {
1062 InterpreterError::Encode(_)
1063 | InterpreterError::Io(_)
1064 | InterpreterError::SpawnFailed { .. } => true,
1065 InterpreterError::UnexpectedResponse(UnexpectedResponseError { path, .. })
1066 | InterpreterError::StatusCode(StatusCodeError { path, .. }) => {
1067 debug!(
1068 "Skipping bad interpreter at {} from {source}: {err}",
1069 path.display()
1070 );
1071 false
1072 }
1073 InterpreterError::QueryScript { path, err } => {
1074 debug!(
1075 "Skipping bad interpreter at {} from {source}: {err}",
1076 path.display()
1077 );
1078 false
1079 }
1080 #[cfg(windows)]
1081 InterpreterError::CorruptWindowsPackage { path, err } => {
1082 debug!(
1083 "Skipping bad interpreter at {} from {source}: {err}",
1084 path.display()
1085 );
1086 false
1087 }
1088 InterpreterError::PermissionDenied { path, err } => {
1089 debug!(
1090 "Skipping unexecutable interpreter at {} from {source}: {err}",
1091 path.display()
1092 );
1093 false
1094 }
1095 InterpreterError::NotFound(path)
1096 | InterpreterError::BrokenLink(BrokenLink { path, .. }) => {
1097 if matches!(source, PythonSource::ActiveEnvironment)
1100 && uv_fs::is_virtualenv_executable(path)
1101 {
1102 true
1103 } else {
1104 trace!("Skipping missing interpreter at {}", path.display());
1105 false
1106 }
1107 }
1108 },
1109 Self::VirtualEnv(VirtualEnvError::MissingPyVenvCfg(path)) => {
1110 trace!("Skipping broken virtualenv at {}", path.display());
1111 false
1112 }
1113 _ => true,
1114 }
1115 }
1116}
1117
1118fn python_installation_from_directory(
1120 path: &PathBuf,
1121 cache: &Cache,
1122) -> Result<PythonInstallation, crate::interpreter::Error> {
1123 let executable = virtualenv_python_executable(path);
1124 Ok(PythonInstallation {
1125 source: PythonSource::ProvidedPath,
1126 interpreter: Interpreter::query(&executable, cache)?,
1127 })
1128}
1129
1130fn python_executables_with_name(
1132 name: &str,
1133) -> impl Iterator<Item = Result<(PythonSource, PathBuf), Error>> + '_ {
1134 which_all(name)
1135 .into_iter()
1136 .flat_map(|inner| inner.map(|path| Ok((PythonSource::SearchPath, path))))
1137}
1138
1139fn python_installations_with_name<'a>(
1141 name: &'a str,
1142 cache: &'a Cache,
1143 strategy: QueryStrategy,
1144) -> Box<dyn Iterator<Item = Result<PythonInstallation, Error>> + 'a> {
1145 python_installations_from_executables(
1146 python_executables_with_name(name)
1147 .map_ok(|executable| PythonExecutableGroup(vec![executable])),
1148 cache,
1149 strategy,
1150 )
1151}
1152
1153pub(crate) fn find_python_installations<'a>(
1155 request: &'a PythonRequest,
1156 environments: EnvironmentPreference,
1157 preference: PythonPreference,
1158 cache: &'a Cache,
1159) -> Box<dyn Iterator<Item = Result<FindPythonResult, Error>> + 'a> {
1160 find_python_installations_with_strategy(
1161 request,
1162 environments,
1163 preference,
1164 cache,
1165 QueryStrategy::Sequential,
1166 )
1167}
1168
1169fn find_python_installations_with_strategy<'a>(
1172 request: &'a PythonRequest,
1173 environments: EnvironmentPreference,
1174 preference: PythonPreference,
1175 cache: &'a Cache,
1176 strategy: QueryStrategy,
1177) -> Box<dyn Iterator<Item = Result<FindPythonResult, Error>> + 'a> {
1178 let sources = DiscoveryPreferences {
1179 python_preference: preference,
1180 environment_preference: environments,
1181 }
1182 .sources(request);
1183
1184 match request {
1185 PythonRequest::File(path) => Box::new(iter::once({
1186 if preference.allows_source(PythonSource::ProvidedPath) {
1187 debug!("Checking for Python interpreter at {request}");
1188 match Interpreter::query(path, cache) {
1189 Ok(interpreter) => Ok(Ok(PythonInstallation {
1190 source: PythonSource::ProvidedPath,
1191 interpreter,
1192 })),
1193 Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1194 Ok(Err(PythonNotFound {
1195 request: request.clone(),
1196 python_preference: preference,
1197 environment_preference: environments,
1198 }))
1199 }
1200 Err(err) => Err(Error::Query(
1201 Box::new(err),
1202 path.clone(),
1203 PythonSource::ProvidedPath,
1204 )),
1205 }
1206 } else {
1207 Err(Error::SourceNotAllowed(
1208 request.clone(),
1209 PythonSource::ProvidedPath,
1210 preference,
1211 ))
1212 }
1213 })),
1214 PythonRequest::Directory(path) => Box::new(iter::once({
1215 if preference.allows_source(PythonSource::ProvidedPath) {
1216 debug!("Checking for Python interpreter in {request}");
1217 match python_installation_from_directory(path, cache) {
1218 Ok(installation) => Ok(Ok(installation)),
1219 Err(InterpreterError::NotFound(_) | InterpreterError::BrokenLink(_)) => {
1220 Ok(Err(PythonNotFound {
1221 request: request.clone(),
1222 python_preference: preference,
1223 environment_preference: environments,
1224 }))
1225 }
1226 Err(err) => Err(Error::Query(
1227 Box::new(err),
1228 path.clone(),
1229 PythonSource::ProvidedPath,
1230 )),
1231 }
1232 } else {
1233 Err(Error::SourceNotAllowed(
1234 request.clone(),
1235 PythonSource::ProvidedPath,
1236 preference,
1237 ))
1238 }
1239 })),
1240 PythonRequest::ExecutableName(name) => {
1241 if preference.allows_source(PythonSource::SearchPath) {
1242 debug!("Searching for Python interpreter with {request}");
1243 Box::new(
1244 python_installations_with_name(name, cache, strategy)
1245 .filter_ok(move |installation| {
1246 environments.allows_installation(installation)
1247 })
1248 .map_ok(Ok),
1249 )
1250 } else {
1251 Box::new(iter::once(Err(Error::SourceNotAllowed(
1252 request.clone(),
1253 PythonSource::SearchPath,
1254 preference,
1255 ))))
1256 }
1257 }
1258 PythonRequest::Any => Box::new({
1259 debug!("Searching for any Python interpreter in {sources}");
1260 python_installations(
1261 &VersionRequest::Any,
1262 None,
1263 PlatformRequest::default(),
1264 environments,
1265 preference,
1266 cache,
1267 strategy,
1268 )
1269 .map_ok(Ok)
1270 }),
1271 PythonRequest::Default => Box::new({
1272 debug!("Searching for default Python interpreter in {sources}");
1273 python_installations(
1274 &VersionRequest::Default,
1275 None,
1276 PlatformRequest::default(),
1277 environments,
1278 preference,
1279 cache,
1280 strategy,
1281 )
1282 .map_ok(Ok)
1283 }),
1284 PythonRequest::Version(version) => {
1285 if let Err(err) = version.check_supported() {
1286 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1287 }
1288 Box::new({
1289 debug!("Searching for {request} in {sources}");
1290 python_installations(
1291 version,
1292 None,
1293 PlatformRequest::default(),
1294 environments,
1295 preference,
1296 cache,
1297 strategy,
1298 )
1299 .map_ok(Ok)
1300 })
1301 }
1302 PythonRequest::Implementation(implementation) => Box::new({
1303 debug!("Searching for a {request} interpreter in {sources}");
1304 python_installations(
1305 &VersionRequest::Default,
1306 Some(implementation),
1307 PlatformRequest::default(),
1308 environments,
1309 preference,
1310 cache,
1311 strategy,
1312 )
1313 .filter_ok(|installation| implementation.matches_interpreter(&installation.interpreter))
1314 .map_ok(Ok)
1315 }),
1316 PythonRequest::ImplementationVersion(implementation, version) => {
1317 if let Err(err) = version.check_supported() {
1318 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1319 }
1320 Box::new({
1321 debug!("Searching for {request} in {sources}");
1322 python_installations(
1323 version,
1324 Some(implementation),
1325 PlatformRequest::default(),
1326 environments,
1327 preference,
1328 cache,
1329 strategy,
1330 )
1331 .filter_ok(|installation| {
1332 implementation.matches_interpreter(&installation.interpreter)
1333 })
1334 .map_ok(Ok)
1335 })
1336 }
1337 PythonRequest::Key(request) => {
1338 if let Some(version) = request.version()
1339 && let Err(err) = version.check_supported()
1340 {
1341 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
1342 }
1343
1344 Box::new({
1345 debug!("Searching for {request} in {sources}");
1346 python_installations(
1347 request.version().unwrap_or(&VersionRequest::Default),
1348 request.implementation(),
1349 request.platform(),
1350 environments,
1351 preference,
1352 cache,
1353 strategy,
1354 )
1355 .filter_ok(move |installation| {
1356 request.satisfied_by_interpreter(&installation.interpreter)
1357 })
1358 .map_ok(Ok)
1359 })
1360 }
1361 }
1362}
1363
1364pub fn find_all_python_installations(
1371 request: &PythonRequest,
1372 environments: EnvironmentPreference,
1373 preference: PythonPreference,
1374 cache: &Cache,
1375) -> Result<Vec<PythonInstallation>, Error> {
1376 let results = find_python_installations_with_strategy(
1377 request,
1378 environments,
1379 preference,
1380 cache,
1381 QueryStrategy::Parallel,
1382 );
1383 let mut installations = Vec::new();
1384 for result in results {
1385 match result {
1386 Ok(Ok(installation)) => installations.push(installation),
1387 Ok(Err(_)) => {}
1388 Err(err @ Error::Query(..)) => {
1389 if uv_warnings::ENABLED.load(Ordering::Relaxed) {
1390 write_warning_chain(&err, Hints::none())
1391 .expect("writing to stderr should not fail");
1392 }
1393 }
1394 Err(err) if err.is_critical() => return Err(err),
1395 Err(_) => {}
1396 }
1397 }
1398 Ok(installations)
1399}
1400
1401pub(crate) fn find_python_installation(
1406 request: &PythonRequest,
1407 environments: EnvironmentPreference,
1408 preference: PythonPreference,
1409 cache: &Cache,
1410) -> Result<FindPythonResult, Error> {
1411 let installations = find_python_installations(request, environments, preference, cache);
1412 let mut first_prerelease = None;
1413 let mut first_debug = None;
1414 let mut first_managed = None;
1415 let mut first_error = None;
1416 for result in installations {
1417 if !result.as_ref().err().is_none_or(Error::is_critical) {
1419 if first_error.is_none()
1421 && let Err(err) = result
1422 {
1423 first_error = Some(err);
1424 }
1425 continue;
1426 }
1427
1428 let Ok(Ok(ref installation)) = result else {
1430 return result;
1431 };
1432
1433 let has_default_executable_name = installation.interpreter.has_default_executable_name()
1439 && matches!(
1440 installation.source,
1441 PythonSource::SearchPath | PythonSource::SearchPathFirst
1442 );
1443
1444 if installation.python_version().pre().is_some()
1447 && !request.allows_prereleases()
1448 && !installation.source.allows_prereleases()
1449 && !has_default_executable_name
1450 {
1451 debug!("Skipping pre-release installation {}", installation.key());
1452 if first_prerelease.is_none() {
1453 first_prerelease = Some(installation.clone());
1454 }
1455 continue;
1456 }
1457
1458 if installation.key().variant().is_debug()
1461 && !request.allows_debug()
1462 && !installation.source.allows_debug()
1463 && !has_default_executable_name
1464 {
1465 debug!("Skipping debug installation {}", installation.key());
1466 if first_debug.is_none() {
1467 first_debug = Some(installation.clone());
1468 }
1469 continue;
1470 }
1471
1472 if installation.is_alternative_implementation()
1477 && !request.allows_alternative_implementations()
1478 && !installation.source.allows_alternative_implementations()
1479 && !has_default_executable_name
1480 {
1481 debug!("Skipping alternative implementation {}", installation.key());
1482 continue;
1483 }
1484
1485 if matches!(preference, PythonPreference::System) && installation.is_managed() {
1488 debug!(
1489 "Skipping managed installation {}: system installation preferred",
1490 installation.key()
1491 );
1492 if first_managed.is_none() {
1493 first_managed = Some(installation.clone());
1494 }
1495 continue;
1496 }
1497
1498 return result;
1500 }
1501
1502 if let Some(installation) = first_managed {
1505 debug!(
1506 "Allowing managed installation {}: no system installations",
1507 installation.key()
1508 );
1509 return Ok(Ok(installation));
1510 }
1511
1512 if let Some(installation) = first_debug {
1515 debug!(
1516 "Allowing debug installation {}: no non-debug installations",
1517 installation.key()
1518 );
1519 return Ok(Ok(installation));
1520 }
1521
1522 if let Some(installation) = first_prerelease {
1524 debug!(
1525 "Allowing pre-release installation {}: no stable installations",
1526 installation.key()
1527 );
1528 return Ok(Ok(installation));
1529 }
1530
1531 if let Some(err) = first_error {
1534 return Err(err);
1535 }
1536
1537 Ok(Err(PythonNotFound {
1538 request: request.clone(),
1539 environment_preference: environments,
1540 python_preference: preference,
1541 }))
1542}
1543
1544#[instrument(skip_all, fields(request))]
1558pub(crate) async fn find_best_python_installation(
1559 request: &PythonRequest,
1560 environments: EnvironmentPreference,
1561 preference: PythonPreference,
1562 downloads_enabled: bool,
1563 client_builder: &BaseClientBuilder<'_>,
1564 cache: &Cache,
1565 reporter: Option<&dyn crate::downloads::Reporter>,
1566 python_install_mirror: Option<&str>,
1567 pypy_install_mirror: Option<&str>,
1568 python_downloads_json_url: Option<&str>,
1569) -> Result<PythonInstallation, crate::Error> {
1570 debug!("Starting Python discovery for {request}");
1571 let original_request = request;
1572
1573 let mut previous_fetch_failed = false;
1574 let mut download_state = None;
1575
1576 let request_without_patch = match request {
1577 PythonRequest::Version(version) => {
1578 if version.has_patch() {
1579 Some(PythonRequest::Version(version.clone().without_patch()))
1580 } else {
1581 None
1582 }
1583 }
1584 PythonRequest::ImplementationVersion(implementation, version) => Some(
1585 PythonRequest::ImplementationVersion(*implementation, version.clone().without_patch()),
1586 ),
1587 _ => None,
1588 };
1589
1590 for (attempt, request) in iter::once(original_request)
1591 .chain(request_without_patch.iter())
1592 .chain(iter::once(&PythonRequest::Default))
1593 .enumerate()
1594 {
1595 debug!(
1596 "Looking for {request}{}",
1597 if request != original_request {
1598 format!(" attempt {attempt} (fallback after failing to find: {original_request})")
1599 } else {
1600 String::new()
1601 }
1602 );
1603 let result = find_python_installation(request, environments, preference, cache);
1604 let error = match result {
1605 Ok(Ok(installation)) => {
1606 warn_on_unsupported_python(installation.interpreter());
1607 return Ok(installation);
1608 }
1609 Ok(Err(error)) => error.into(),
1611 Err(error) if !error.is_critical() => error.into(),
1612 Err(error) => return Err(error.into()),
1613 };
1614
1615 if downloads_enabled
1617 && !previous_fetch_failed
1618 && let Some(download_request) = PythonDownloadRequest::from_request(request)
1619 {
1620 let (client, retry_policy, download_list) =
1621 if let Some(download_state) = &mut download_state {
1622 download_state
1623 } else {
1624 let download_list = ManagedPythonDownloadList::new(
1625 client_builder,
1626 cache,
1627 python_downloads_json_url,
1628 )
1629 .await?;
1630 let retry_policy = client_builder.retry_policy();
1631
1632 let client = client_builder.clone().retries(0).build()?;
1635 download_state.insert((client, retry_policy, download_list))
1636 };
1637
1638 let download = download_request
1639 .clone()
1640 .fill()
1641 .map(|request| download_list.find(&request));
1642
1643 let result = match download {
1644 Ok(Ok(download)) => PythonInstallation::fetch(
1645 download,
1646 client,
1647 retry_policy,
1648 cache,
1649 reporter,
1650 python_install_mirror,
1651 pypy_install_mirror,
1652 )
1653 .await
1654 .map(Some),
1655 Ok(Err(crate::downloads::Error::NoDownloadFound(_))) => Ok(None),
1656 Ok(Err(error)) => Err(error.into()),
1657 Err(error) => Err(error.into()),
1658 };
1659 if let Ok(Some(installation)) = result {
1660 return Ok(installation);
1661 }
1662 if let Err(error) = result {
1670 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1674 return Err(error);
1675 }
1676
1677 let error = anyhow::Error::from(error).context(format!(
1678 "A managed Python download is available for {request}, but an error occurred when attempting to download it."
1679 ));
1680 write_warning_chain(error.as_ref(), Hints::none())
1681 .expect("writing to stderr should not fail");
1682 previous_fetch_failed = true;
1683 }
1684 }
1685
1686 if matches!(request, PythonRequest::Default | PythonRequest::Any) {
1692 return Err(match error {
1693 crate::Error::MissingPython(err, _) => PythonNotFound {
1694 request: original_request.clone(),
1696 python_preference: err.python_preference,
1697 environment_preference: err.environment_preference,
1698 }
1699 .into(),
1700 other => other,
1701 });
1702 }
1703 }
1704
1705 unreachable!("The loop should have terminated when it reached PythonRequest::Default");
1706}
1707
1708fn warn_on_unsupported_python(interpreter: &Interpreter) {
1710 if interpreter.python_tuple() < (3, 8) {
1712 warn_user_once!(
1713 "uv is only compatible with Python >=3.8, found Python {}",
1714 interpreter.python_version()
1715 );
1716 }
1717}
1718
1719#[cfg(windows)]
1736fn is_windows_store_shim(path: &Path) -> bool {
1737 use std::os::windows::fs::MetadataExt;
1738 use std::os::windows::prelude::OsStrExt;
1739 use windows::Win32::Foundation::CloseHandle;
1740 use windows::Win32::Storage::FileSystem::{
1741 CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
1742 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_MODE, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
1743 OPEN_EXISTING,
1744 };
1745 use windows::Win32::System::IO::DeviceIoControl;
1746 use windows::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
1747 use windows::core::PCWSTR;
1748
1749 if !path.is_absolute() {
1751 return false;
1752 }
1753
1754 let mut components = path.components().rev();
1757
1758 if !components
1760 .next()
1761 .and_then(|component| component.as_os_str().to_str())
1762 .is_some_and(|component| {
1763 component.starts_with("python")
1764 && std::path::Path::new(component)
1765 .extension()
1766 .is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
1767 })
1768 {
1769 return false;
1770 }
1771
1772 if components
1774 .next()
1775 .is_none_or(|component| component.as_os_str() != "WindowsApps")
1776 {
1777 return false;
1778 }
1779
1780 if components
1782 .next()
1783 .is_none_or(|component| component.as_os_str() != "Microsoft")
1784 {
1785 return false;
1786 }
1787
1788 let Ok(md) = fs_err::symlink_metadata(path) else {
1790 return false;
1791 };
1792 if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 {
1793 return false;
1794 }
1795
1796 let mut path_encoded = path
1797 .as_os_str()
1798 .encode_wide()
1799 .chain(std::iter::once(0))
1800 .collect::<Vec<_>>();
1801
1802 #[allow(unsafe_code)]
1804 let reparse_handle = unsafe {
1805 CreateFileW(
1806 PCWSTR(path_encoded.as_mut_ptr()),
1807 0,
1808 FILE_SHARE_MODE(0),
1809 None,
1810 OPEN_EXISTING,
1811 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
1812 None,
1813 )
1814 };
1815
1816 let Ok(reparse_handle) = reparse_handle else {
1817 return false;
1818 };
1819
1820 let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
1821 let mut bytes_returned = 0;
1822
1823 #[allow(unsafe_code, clippy::cast_possible_truncation)]
1825 let success = unsafe {
1826 DeviceIoControl(
1827 reparse_handle,
1828 FSCTL_GET_REPARSE_POINT,
1829 None,
1830 0,
1831 Some(buf.as_mut_ptr().cast()),
1832 buf.len() as u32 * 2,
1833 Some(&raw mut bytes_returned),
1834 None,
1835 )
1836 .is_ok()
1837 };
1838
1839 #[allow(unsafe_code)]
1841 unsafe {
1842 let _ = CloseHandle(reparse_handle);
1843 }
1844
1845 if !success {
1847 return false;
1848 }
1849
1850 let reparse_point = String::from_utf16_lossy(&buf[..bytes_returned as usize]);
1851 reparse_point.contains("\\AppInstallerPythonRedirector.exe")
1852}
1853
1854#[cfg(not(windows))]
1858fn is_windows_store_shim(_path: &Path) -> bool {
1859 false
1860}
1861
1862impl PythonVariant {
1863 fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
1864 match self {
1865 Self::Default => {
1866 if (interpreter.python_major(), interpreter.python_minor()) >= (3, 14) {
1869 true
1872 } else {
1873 !interpreter.gil_disabled()
1876 }
1877 }
1878 Self::Debug => interpreter.debug_enabled(),
1879 Self::Freethreaded => interpreter.gil_disabled(),
1880 Self::FreethreadedDebug => interpreter.gil_disabled() && interpreter.debug_enabled(),
1881 Self::Gil => !interpreter.gil_disabled(),
1882 Self::GilDebug => !interpreter.gil_disabled() && interpreter.debug_enabled(),
1883 }
1884 }
1885
1886 pub fn executable_suffix(self) -> &'static str {
1890 match self {
1891 Self::Default => "",
1892 Self::Debug => "d",
1893 Self::Freethreaded => "t",
1894 Self::FreethreadedDebug => "td",
1895 Self::Gil => "",
1896 Self::GilDebug => "d",
1897 }
1898 }
1899
1900 pub fn display_suffix(self) -> &'static str {
1902 match self {
1903 Self::Default => "",
1904 Self::Debug => "+debug",
1905 Self::Freethreaded => "+freethreaded",
1906 Self::FreethreadedDebug => "+freethreaded+debug",
1907 Self::Gil => "+gil",
1908 Self::GilDebug => "+gil+debug",
1909 }
1910 }
1911
1912 pub(crate) fn lib_suffix(self) -> &'static str {
1915 match self {
1916 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => "",
1917 Self::Freethreaded | Self::FreethreadedDebug => "t",
1918 }
1919 }
1920
1921 fn is_freethreaded(self) -> bool {
1922 match self {
1923 Self::Default | Self::Debug | Self::Gil | Self::GilDebug => false,
1924 Self::Freethreaded | Self::FreethreadedDebug => true,
1925 }
1926 }
1927
1928 pub fn is_debug(self) -> bool {
1929 match self {
1930 Self::Default | Self::Freethreaded | Self::Gil => false,
1931 Self::Debug | Self::FreethreadedDebug | Self::GilDebug => true,
1932 }
1933 }
1934}
1935impl PythonRequest {
1936 pub fn from_requires_python(requires_python: &RequiresPython) -> Option<Self> {
1938 let specifiers = requires_python.specifiers().clone();
1939 if specifiers.is_empty() {
1940 return None;
1941 }
1942
1943 Some(Self::Version(VersionRequest::from_specifiers(
1944 specifiers,
1945 PythonVariant::Default,
1946 )))
1947 }
1948
1949 pub fn parse(value: &str) -> Self {
1957 let lowercase_value = &value.to_ascii_lowercase();
1958
1959 if lowercase_value == "any" {
1961 return Self::Any;
1962 }
1963 if lowercase_value == "default" {
1964 return Self::Default;
1965 }
1966
1967 let abstract_version_prefixes = ["python", ""];
1969 let all_implementation_names = ImplementationName::iter_all().flat_map(|implementation| {
1970 std::iter::once(implementation.long_name()).chain(implementation.short_name())
1971 });
1972 if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1975 abstract_version_prefixes,
1976 all_implementation_names,
1977 lowercase_value,
1978 ) {
1979 return request;
1980 }
1981
1982 let value_as_path = PathBuf::from(value);
1983 if value_as_path.is_dir() {
1985 return Self::Directory(value_as_path);
1986 }
1987 if value_as_path.is_file() {
1989 return Self::File(value_as_path);
1990 }
1991
1992 #[cfg(windows)]
1994 if value_as_path.extension().is_none() {
1995 let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1996 if value_as_path.is_file() {
1997 return Self::File(value_as_path);
1998 }
1999 }
2000
2001 #[cfg(test)]
2006 if value_as_path.is_relative() {
2007 if let Ok(current_dir) = crate::current_dir() {
2008 let relative = current_dir.join(&value_as_path);
2009 if relative.is_dir() {
2010 return Self::Directory(relative);
2011 }
2012 if relative.is_file() {
2013 return Self::File(relative);
2014 }
2015 }
2016 }
2017 if value.contains(std::path::MAIN_SEPARATOR) {
2020 return Self::File(value_as_path);
2021 }
2022 if cfg!(windows) && value.contains('/') {
2025 return Self::File(value_as_path);
2026 }
2027 if let Ok(request) = PythonDownloadRequest::from_str(value) {
2028 return Self::Key(request);
2029 }
2030 Self::ExecutableName(value.to_string())
2033 }
2034
2035 pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
2049 let lowercase_value = &value.to_ascii_lowercase();
2050 let abstract_version_prefixes = if cfg!(windows) {
2052 &["python", "pythonw"][..]
2053 } else {
2054 &["python"][..]
2055 };
2056 if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
2058 return Ok(Some(Self::Default));
2059 }
2060 Self::parse_versions_and_implementations(
2061 abstract_version_prefixes.iter().copied(),
2062 ImplementationName::iter_all().map(ImplementationName::long_name),
2063 lowercase_value,
2064 )
2065 }
2066
2067 fn parse_versions_and_implementations<'a>(
2076 abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
2078 implementation_names: impl IntoIterator<Item = &'a str>,
2080 lowercase_value: &str,
2082 ) -> Result<Option<Self>, Error> {
2083 for prefix in abstract_version_prefixes {
2084 if let Some(version_request) =
2085 Self::try_split_prefix_and_version(prefix, lowercase_value)?
2086 {
2087 return Ok(Some(Self::Version(version_request)));
2091 }
2092 }
2093 for implementation in implementation_names {
2094 if lowercase_value == implementation {
2095 return Ok(Some(Self::Implementation(
2096 ImplementationName::from_str(implementation).unwrap(),
2099 )));
2100 }
2101 if let Some(version_request) =
2102 Self::try_split_prefix_and_version(implementation, lowercase_value)?
2103 {
2104 return Ok(Some(Self::ImplementationVersion(
2106 ImplementationName::from_str(implementation).unwrap(),
2108 version_request,
2109 )));
2110 }
2111 }
2112 Ok(None)
2113 }
2114
2115 fn try_split_prefix_and_version(
2126 prefix: &str,
2127 lowercase_value: &str,
2128 ) -> Result<Option<VersionRequest>, Error> {
2129 if lowercase_value.starts_with('@') {
2130 return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
2131 }
2132 let Some(rest) = lowercase_value.strip_prefix(prefix) else {
2133 return Ok(None);
2134 };
2135 if rest.is_empty() {
2137 return Ok(None);
2138 }
2139 if let Some(after_at) = rest.strip_prefix('@') {
2142 if after_at == "latest" {
2143 return Err(Error::LatestVersionRequest);
2146 }
2147 return after_at.parse().map(Some);
2148 }
2149 Ok(rest.parse().ok())
2152 }
2153
2154 pub fn includes_patch(&self) -> bool {
2156 match self {
2157 Self::Default => false,
2158 Self::Any => false,
2159 Self::Version(version_request) => version_request.patch().is_some(),
2160 Self::Directory(..) => false,
2161 Self::File(..) => false,
2162 Self::ExecutableName(..) => false,
2163 Self::Implementation(..) => false,
2164 Self::ImplementationVersion(_, version) => version.patch().is_some(),
2165 Self::Key(request) => request
2166 .version
2167 .as_ref()
2168 .is_some_and(|request| request.patch().is_some()),
2169 }
2170 }
2171
2172 pub fn includes_prerelease(&self) -> bool {
2174 match self {
2175 Self::Default => false,
2176 Self::Any => false,
2177 Self::Version(version_request) => version_request.prerelease().is_some(),
2178 Self::Directory(..) => false,
2179 Self::File(..) => false,
2180 Self::ExecutableName(..) => false,
2181 Self::Implementation(..) => false,
2182 Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
2183 Self::Key(request) => request
2184 .version
2185 .as_ref()
2186 .is_some_and(|request| request.prerelease().is_some()),
2187 }
2188 }
2189
2190 pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
2192 fn is_same_executable(path1: &Path, path2: &Path) -> bool {
2194 path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
2195 }
2196
2197 match self {
2198 Self::Default | Self::Any => true,
2199 Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2200 Self::Directory(directory) => {
2201 is_same_executable(directory, interpreter.sys_prefix())
2203 || is_same_executable(
2204 virtualenv_python_executable(directory).as_path(),
2205 interpreter.sys_executable(),
2206 )
2207 }
2208 Self::File(file) => {
2209 if is_same_executable(interpreter.sys_executable(), file) {
2211 return true;
2212 }
2213 if interpreter
2215 .sys_base_executable()
2216 .is_some_and(|sys_base_executable| {
2217 is_same_executable(sys_base_executable, file)
2218 })
2219 {
2220 return true;
2221 }
2222 if cfg!(windows) {
2227 if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2228 if let (Some(file_base), Some(interpreter_base)) = (
2229 file_interpreter.sys_base_executable(),
2230 interpreter.sys_base_executable(),
2231 ) {
2232 if is_same_executable(file_base, interpreter_base) {
2233 return true;
2234 }
2235 }
2236 }
2237 }
2238 false
2239 }
2240 Self::ExecutableName(name) => {
2241 if interpreter
2243 .sys_executable()
2244 .file_name()
2245 .is_some_and(|filename| filename == name.as_str())
2246 {
2247 return true;
2248 }
2249 if interpreter
2251 .sys_base_executable()
2252 .and_then(|executable| executable.file_name())
2253 .is_some_and(|file_name| file_name == name.as_str())
2254 {
2255 return true;
2256 }
2257 if which(name)
2260 .ok()
2261 .as_ref()
2262 .and_then(|executable| executable.file_name())
2263 .is_some_and(|file_name| file_name == name.as_str())
2264 {
2265 return true;
2266 }
2267 false
2268 }
2269 Self::Implementation(implementation) => interpreter
2270 .implementation_name()
2271 .eq_ignore_ascii_case(implementation.long_name()),
2272 Self::ImplementationVersion(implementation, version) => {
2273 version.matches_interpreter(interpreter)
2274 && interpreter
2275 .implementation_name()
2276 .eq_ignore_ascii_case(implementation.long_name())
2277 }
2278 Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2279 }
2280 }
2281
2282 pub(crate) fn allows_prereleases(&self) -> bool {
2284 match self {
2285 Self::Default => false,
2286 Self::Any => true,
2287 Self::Version(version) => version.allows_prereleases(),
2288 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2289 Self::Implementation(_) => false,
2290 Self::ImplementationVersion(_, _) => true,
2291 Self::Key(request) => request.allows_prereleases(),
2292 }
2293 }
2294
2295 fn allows_debug(&self) -> bool {
2297 match self {
2298 Self::Default => false,
2299 Self::Any => true,
2300 Self::Version(version) => version.is_debug(),
2301 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2302 Self::Implementation(_) => false,
2303 Self::ImplementationVersion(_, _) => true,
2304 Self::Key(request) => request.allows_debug(),
2305 }
2306 }
2307
2308 fn allows_alternative_implementations(&self) -> bool {
2310 match self {
2311 Self::Default => false,
2312 Self::Any => true,
2313 Self::Version(_) => false,
2314 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2315 Self::Implementation(implementation)
2316 | Self::ImplementationVersion(implementation, _) => {
2317 !matches!(implementation, ImplementationName::CPython)
2318 }
2319 Self::Key(request) => request.allows_alternative_implementations(),
2320 }
2321 }
2322
2323 pub(crate) fn is_explicit_system(&self) -> bool {
2324 matches!(self, Self::File(_) | Self::Directory(_))
2325 }
2326
2327 pub fn to_canonical_string(&self) -> Cow<'_, str> {
2331 match self {
2332 Self::Any => Cow::Borrowed("any"),
2333 Self::Default => Cow::Borrowed("default"),
2334 Self::Version(version) => Cow::Owned(version.to_string()),
2335 Self::Directory(path) | Self::File(path) => path.to_string_lossy(),
2336 Self::ExecutableName(name) => Cow::Borrowed(name),
2337 Self::Implementation(implementation) => Cow::Borrowed(implementation.long_name()),
2338 Self::ImplementationVersion(implementation, version) => {
2339 Cow::Owned(format!("{implementation}@{version}"))
2340 }
2341 Self::Key(request) => Cow::Owned(request.to_string()),
2342 }
2343 }
2344
2345 pub fn as_pep440_version(&self) -> Option<Version> {
2349 match self {
2350 Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2351 Self::Key(download_request) => download_request
2352 .version()
2353 .and_then(VersionRequest::as_pep440_version),
2354 Self::Default
2355 | Self::Any
2356 | Self::Directory(_)
2357 | Self::File(_)
2358 | Self::ExecutableName(_)
2359 | Self::Implementation(_) => None,
2360 }
2361 }
2362
2363 fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2369 match self {
2370 Self::Version(version) | Self::ImplementationVersion(_, version) => {
2371 version.as_version_specifiers()
2372 }
2373 Self::Key(download_request) => download_request
2374 .version()
2375 .and_then(VersionRequest::as_version_specifiers),
2376 Self::Default
2377 | Self::Any
2378 | Self::Directory(_)
2379 | Self::File(_)
2380 | Self::ExecutableName(_)
2381 | Self::Implementation(_) => None,
2382 }
2383 }
2384
2385 pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2391 let Some(specifiers) = self.as_version_specifiers() else {
2392 return true;
2393 };
2394
2395 let request_range = release_specifiers_to_ranges(specifiers);
2396 let requires_python_range =
2397 release_specifiers_to_ranges(requires_python.specifiers().clone());
2398 !request_range
2399 .intersection(&requires_python_range)
2400 .is_empty()
2401 }
2402}
2403
2404impl PythonSource {
2405 pub fn is_managed(self) -> bool {
2406 matches!(self, Self::Managed)
2407 }
2408
2409 fn allows_prereleases(self) -> bool {
2411 match self {
2412 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2413 Self::SearchPath
2414 | Self::SearchPathFirst
2415 | Self::CondaPrefix
2416 | Self::BaseCondaPrefix
2417 | Self::ProvidedPath
2418 | Self::ParentInterpreter
2419 | Self::ActiveEnvironment
2420 | Self::DiscoveredEnvironment => true,
2421 }
2422 }
2423
2424 fn allows_debug(self) -> bool {
2426 match self {
2427 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2428 Self::SearchPath
2429 | Self::SearchPathFirst
2430 | Self::CondaPrefix
2431 | Self::BaseCondaPrefix
2432 | Self::ProvidedPath
2433 | Self::ParentInterpreter
2434 | Self::ActiveEnvironment
2435 | Self::DiscoveredEnvironment => true,
2436 }
2437 }
2438
2439 fn allows_alternative_implementations(self) -> bool {
2441 match self {
2442 Self::Managed
2443 | Self::Registry
2444 | Self::SearchPath
2445 | Self::SearchPathFirst
2448 | Self::MicrosoftStore => false,
2449 Self::CondaPrefix
2450 | Self::BaseCondaPrefix
2451 | Self::ProvidedPath
2452 | Self::ParentInterpreter
2453 | Self::ActiveEnvironment
2454 | Self::DiscoveredEnvironment => true,
2455 }
2456 }
2457
2458 fn is_maybe_virtualenv(self) -> bool {
2470 match self {
2471 Self::ProvidedPath
2472 | Self::ActiveEnvironment
2473 | Self::DiscoveredEnvironment
2474 | Self::CondaPrefix
2475 | Self::BaseCondaPrefix
2476 | Self::ParentInterpreter
2477 | Self::SearchPathFirst => true,
2478 Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2479 }
2480 }
2481
2482 fn is_explicit(self) -> bool {
2485 match self {
2486 Self::ProvidedPath
2487 | Self::ParentInterpreter
2488 | Self::ActiveEnvironment
2489 | Self::CondaPrefix => true,
2490 Self::Managed
2491 | Self::DiscoveredEnvironment
2492 | Self::SearchPath
2493 | Self::SearchPathFirst
2494 | Self::Registry
2495 | Self::MicrosoftStore
2496 | Self::BaseCondaPrefix => false,
2497 }
2498 }
2499
2500 fn is_maybe_system(self) -> bool {
2502 match self {
2503 Self::CondaPrefix
2504 | Self::BaseCondaPrefix
2505 | Self::ParentInterpreter
2506 | Self::ProvidedPath
2507 | Self::Managed
2508 | Self::SearchPath
2509 | Self::SearchPathFirst
2510 | Self::Registry
2511 | Self::MicrosoftStore => true,
2512 Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2513 }
2514 }
2515}
2516
2517impl PythonPreference {
2518 fn allows_source(self, source: PythonSource) -> bool {
2519 if !matches!(
2521 source,
2522 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2523 ) {
2524 return true;
2525 }
2526
2527 match self {
2528 Self::OnlyManaged => matches!(source, PythonSource::Managed),
2529 Self::Managed | Self::System => matches!(
2530 source,
2531 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2532 ),
2533 Self::OnlySystem => {
2534 matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2535 }
2536 }
2537 }
2538
2539 pub(crate) fn allows_managed(self) -> bool {
2540 match self {
2541 Self::OnlySystem => false,
2542 Self::Managed | Self::System | Self::OnlyManaged => true,
2543 }
2544 }
2545
2546 fn allows_interpreter(self, interpreter: &Interpreter) -> bool {
2551 match self {
2552 Self::OnlyManaged => interpreter.is_managed(),
2553 Self::OnlySystem => !interpreter.is_managed(),
2554 Self::Managed | Self::System => true,
2555 }
2556 }
2557
2558 pub fn allows_installation(self, installation: &PythonInstallation) -> bool {
2566 let source = installation.source;
2567 let interpreter = &installation.interpreter;
2568
2569 match self {
2570 Self::OnlyManaged => {
2571 if self.allows_interpreter(interpreter) {
2572 true
2573 } else if source.is_explicit() {
2574 debug!(
2575 "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2576 interpreter.sys_executable().display()
2577 );
2578 true
2579 } else {
2580 debug!(
2581 "Ignoring Python interpreter at `{}`: only managed interpreters allowed",
2582 interpreter.sys_executable().display()
2583 );
2584 false
2585 }
2586 }
2587 Self::Managed | Self::System => true,
2589 Self::OnlySystem => {
2590 if self.allows_interpreter(interpreter) {
2591 true
2592 } else if source.is_explicit() {
2593 debug!(
2594 "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2595 interpreter.sys_executable().display()
2596 );
2597 true
2598 } else {
2599 debug!(
2600 "Ignoring Python interpreter at `{}`: only system interpreters allowed",
2601 interpreter.sys_executable().display()
2602 );
2603 false
2604 }
2605 }
2606 }
2607 }
2608
2609 #[must_use]
2614 pub fn with_system_flag(self, system: bool) -> Self {
2615 match self {
2616 Self::OnlyManaged => self,
2621 Self::Managed => {
2622 if system {
2623 Self::System
2624 } else {
2625 self
2626 }
2627 }
2628 Self::System => self,
2629 Self::OnlySystem => self,
2630 }
2631 }
2632}
2633
2634impl PythonDownloads {
2635 pub fn is_automatic(self) -> bool {
2636 matches!(self, Self::Automatic)
2637 }
2638}
2639
2640impl EnvironmentPreference {
2641 pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2642 match (system, mutable) {
2643 (true, _) => Self::OnlySystem,
2645 (false, true) => Self::ExplicitSystem,
2647 (false, false) => Self::Any,
2649 }
2650 }
2651
2652 pub(crate) fn allows_installation(self, installation: &PythonInstallation) -> bool {
2658 interpreter_satisfies_environment_preference(
2659 installation.source,
2660 &installation.interpreter,
2661 self,
2662 )
2663 }
2664}
2665
2666#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2667pub(crate) struct ExecutableName {
2668 implementation: Option<ImplementationName>,
2669 major: Option<u8>,
2670 minor: Option<u8>,
2671 patch: Option<u8>,
2672 prerelease: Option<Prerelease>,
2673 variant: PythonVariant,
2674}
2675
2676#[derive(Debug, Clone, PartialEq, Eq)]
2677struct ExecutableNameComparator<'a> {
2678 name: ExecutableName,
2679 request: &'a VersionRequest,
2680 implementation: Option<&'a ImplementationName>,
2681}
2682
2683impl Ord for ExecutableNameComparator<'_> {
2684 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2688 let name_ordering = if self.implementation.is_some() {
2691 std::cmp::Ordering::Greater
2692 } else {
2693 std::cmp::Ordering::Less
2694 };
2695 if self.name.implementation.is_none() && other.name.implementation.is_some() {
2696 return name_ordering.reverse();
2697 }
2698 if self.name.implementation.is_some() && other.name.implementation.is_none() {
2699 return name_ordering;
2700 }
2701 let ordering = self.name.implementation.cmp(&other.name.implementation);
2703 if ordering != std::cmp::Ordering::Equal {
2704 return ordering;
2705 }
2706 let ordering = self.name.major.cmp(&other.name.major);
2707 let is_default_request =
2708 matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2709 if ordering != std::cmp::Ordering::Equal {
2710 return if is_default_request {
2711 ordering.reverse()
2712 } else {
2713 ordering
2714 };
2715 }
2716 let ordering = self.name.minor.cmp(&other.name.minor);
2717 if ordering != std::cmp::Ordering::Equal {
2718 return if is_default_request {
2719 ordering.reverse()
2720 } else {
2721 ordering
2722 };
2723 }
2724 let ordering = self.name.patch.cmp(&other.name.patch);
2725 if ordering != std::cmp::Ordering::Equal {
2726 return if is_default_request {
2727 ordering.reverse()
2728 } else {
2729 ordering
2730 };
2731 }
2732 let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2733 if ordering != std::cmp::Ordering::Equal {
2734 return if is_default_request {
2735 ordering.reverse()
2736 } else {
2737 ordering
2738 };
2739 }
2740 let ordering = self.name.variant.cmp(&other.name.variant);
2741 if ordering != std::cmp::Ordering::Equal {
2742 return if is_default_request {
2743 ordering.reverse()
2744 } else {
2745 ordering
2746 };
2747 }
2748 ordering
2749 }
2750}
2751
2752impl PartialOrd for ExecutableNameComparator<'_> {
2753 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2754 Some(self.cmp(other))
2755 }
2756}
2757
2758impl ExecutableName {
2759 #[must_use]
2760 fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2761 self.implementation = Some(implementation);
2762 self
2763 }
2764
2765 #[must_use]
2766 fn with_major(mut self, major: u8) -> Self {
2767 self.major = Some(major);
2768 self
2769 }
2770
2771 #[must_use]
2772 fn with_minor(mut self, minor: u8) -> Self {
2773 self.minor = Some(minor);
2774 self
2775 }
2776
2777 #[must_use]
2778 fn with_patch(mut self, patch: u8) -> Self {
2779 self.patch = Some(patch);
2780 self
2781 }
2782
2783 #[must_use]
2784 fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2785 self.prerelease = Some(prerelease);
2786 self
2787 }
2788
2789 #[must_use]
2790 fn with_variant(mut self, variant: PythonVariant) -> Self {
2791 self.variant = variant;
2792 self
2793 }
2794
2795 fn into_comparator<'a>(
2796 self,
2797 request: &'a VersionRequest,
2798 implementation: Option<&'a ImplementationName>,
2799 ) -> ExecutableNameComparator<'a> {
2800 ExecutableNameComparator {
2801 name: self,
2802 request,
2803 implementation,
2804 }
2805 }
2806}
2807
2808impl fmt::Display for ExecutableName {
2809 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2810 if let Some(implementation) = self.implementation {
2811 write!(f, "{implementation}")?;
2812 } else {
2813 f.write_str("python")?;
2814 }
2815 if let Some(major) = self.major {
2816 write!(f, "{major}")?;
2817 if let Some(minor) = self.minor {
2818 write!(f, ".{minor}")?;
2819 if let Some(patch) = self.patch {
2820 write!(f, ".{patch}")?;
2821 }
2822 }
2823 }
2824 if let Some(prerelease) = &self.prerelease {
2825 write!(f, "{prerelease}")?;
2826 }
2827 f.write_str(self.variant.executable_suffix())?;
2828 f.write_str(EXE_SUFFIX)?;
2829 Ok(())
2830 }
2831}
2832
2833impl VersionRequest {
2834 pub fn from_specifiers(specifiers: VersionSpecifiers, variant: PythonVariant) -> Self {
2839 if let [specifier] = specifiers.iter().as_slice()
2840 && specifier.operator() == &uv_pep440::Operator::Equal
2841 && let Ok(request) = Self::from_str(&specifier.version().to_string())
2842 {
2843 return request;
2844 }
2845 Self::Range(specifiers, variant)
2846 }
2847
2848 #[must_use]
2850 pub fn only_minor(self) -> Self {
2851 match self {
2852 Self::Any => self,
2853 Self::Default => self,
2854 Self::Range(specifiers, variant) => Self::Range(
2855 specifiers
2856 .into_iter()
2857 .map(|s| s.only_minor_release())
2858 .collect(),
2859 variant,
2860 ),
2861 Self::Major(..) => self,
2862 Self::MajorMinor(..) => self,
2863 Self::MajorMinorPatch(major, minor, _, variant)
2864 | Self::MajorMinorPrerelease(major, minor, _, variant)
2865 | Self::MajorMinorPatchPrerelease(major, minor, _, _, variant) => {
2866 Self::MajorMinor(major, minor, variant)
2867 }
2868 }
2869 }
2870
2871 pub(crate) fn executable_names(
2873 &self,
2874 implementation: Option<&ImplementationName>,
2875 ) -> Vec<ExecutableName> {
2876 let prerelease = match self {
2877 Self::MajorMinorPrerelease(_, _, prerelease, _)
2878 | Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => {
2879 Some(prerelease)
2881 }
2882 _ => None,
2883 };
2884
2885 let mut names = Vec::new();
2887 names.push(ExecutableName::default());
2888
2889 if let Some(major) = self.major() {
2891 names.push(ExecutableName::default().with_major(major));
2893 if let Some(minor) = self.minor() {
2894 names.push(
2896 ExecutableName::default()
2897 .with_major(major)
2898 .with_minor(minor),
2899 );
2900 if let Some(patch) = self.patch() {
2901 names.push(
2903 ExecutableName::default()
2904 .with_major(major)
2905 .with_minor(minor)
2906 .with_patch(patch),
2907 );
2908 }
2909 }
2910 } else {
2911 names.push(ExecutableName::default().with_major(3));
2913 }
2914
2915 if let Some(prerelease) = prerelease {
2916 for i in 0..names.len() {
2918 let name = names[i];
2919 if name.minor.is_none() {
2920 continue;
2923 }
2924 names.push(name.with_prerelease(*prerelease));
2925 }
2926 }
2927
2928 if let Some(implementation) = implementation {
2930 for i in 0..names.len() {
2931 let name = names[i].with_implementation(*implementation);
2932 names.push(name);
2933 }
2934 } else {
2935 if matches!(self, Self::Any) {
2937 for i in 0..names.len() {
2938 for implementation in ImplementationName::iter_all() {
2939 let name = names[i].with_implementation(implementation);
2940 names.push(name);
2941 }
2942 }
2943 }
2944 }
2945
2946 if let Some(variant) = self.variant()
2948 && variant != PythonVariant::Default
2949 {
2950 for i in 0..names.len() {
2951 let name = names[i].with_variant(variant);
2952 names.push(name);
2953 }
2954 }
2955
2956 names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
2957 names.reverse();
2958
2959 names
2960 }
2961
2962 fn major(&self) -> Option<u8> {
2964 match self {
2965 Self::Any | Self::Default | Self::Range(_, _) => None,
2966 Self::Major(major, _) => Some(*major),
2967 Self::MajorMinor(major, _, _) => Some(*major),
2968 Self::MajorMinorPatch(major, _, _, _) => Some(*major),
2969 Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2970 Self::MajorMinorPatchPrerelease(major, _, _, _, _) => Some(*major),
2971 }
2972 }
2973
2974 fn minor(&self) -> Option<u8> {
2976 match self {
2977 Self::Any | Self::Default | Self::Range(_, _) => None,
2978 Self::Major(_, _) => None,
2979 Self::MajorMinor(_, minor, _) => Some(*minor),
2980 Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
2981 Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2982 Self::MajorMinorPatchPrerelease(_, minor, _, _, _) => Some(*minor),
2983 }
2984 }
2985
2986 fn patch(&self) -> Option<u8> {
2988 match self {
2989 Self::Any | Self::Default | Self::Range(_, _) => None,
2990 Self::Major(_, _) => None,
2991 Self::MajorMinor(_, _, _) => None,
2992 Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
2993 Self::MajorMinorPrerelease(_, _, _, _) => None,
2994 Self::MajorMinorPatchPrerelease(_, _, patch, _, _) => Some(*patch),
2995 }
2996 }
2997
2998 fn prerelease(&self) -> Option<&Prerelease> {
3000 match self {
3001 Self::Any | Self::Default | Self::Range(_, _) => None,
3002 Self::Major(_, _) => None,
3003 Self::MajorMinor(_, _, _) => None,
3004 Self::MajorMinorPatch(_, _, _, _) => None,
3005 Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
3006 Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => Some(prerelease),
3007 }
3008 }
3009
3010 fn check_supported(&self) -> Result<(), String> {
3014 match self {
3015 Self::Any | Self::Default => (),
3016 Self::Major(major, _) => {
3017 if *major < 3 {
3018 return Err(format!(
3019 "Python <3 is not supported but {major} was requested."
3020 ));
3021 }
3022 }
3023 Self::MajorMinor(major, minor, _) => {
3024 if (*major, *minor) < (3, 6) {
3025 return Err(format!(
3026 "Python <3.6 is not supported but {major}.{minor} was requested."
3027 ));
3028 }
3029 }
3030 Self::MajorMinorPatch(major, minor, patch, _) => {
3031 if (*major, *minor) < (3, 6) {
3032 return Err(format!(
3033 "Python <3.6 is not supported but {major}.{minor}.{patch} was requested."
3034 ));
3035 }
3036 }
3037 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3038 if (*major, *minor) < (3, 6) {
3039 return Err(format!(
3040 "Python <3.6 is not supported but {major}.{minor}{prerelease} was requested."
3041 ));
3042 }
3043 }
3044 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3045 if (*major, *minor) < (3, 6) {
3046 return Err(format!(
3047 "Python <3.6 is not supported but {major}.{minor}.{patch}{prerelease} was requested."
3048 ));
3049 }
3050 }
3051 Self::Range(_, _) => (),
3053 }
3054
3055 if self.is_freethreaded()
3056 && let Self::MajorMinor(major, minor, _) = self.clone().without_patch()
3057 && (major, minor) < (3, 13)
3058 {
3059 return Err(format!(
3060 "Python <3.13 does not support free-threading but {self} was requested."
3061 ));
3062 }
3063
3064 Ok(())
3065 }
3066
3067 #[must_use]
3073 fn into_request_for_source(self, source: PythonSource) -> Self {
3074 match self {
3075 Self::Default => match source {
3076 PythonSource::ParentInterpreter
3077 | PythonSource::CondaPrefix
3078 | PythonSource::BaseCondaPrefix
3079 | PythonSource::ProvidedPath
3080 | PythonSource::DiscoveredEnvironment
3081 | PythonSource::ActiveEnvironment => Self::Any,
3082 PythonSource::SearchPath
3083 | PythonSource::SearchPathFirst
3084 | PythonSource::Registry
3085 | PythonSource::MicrosoftStore
3086 | PythonSource::Managed => Self::Default,
3087 },
3088 _ => self,
3089 }
3090 }
3091
3092 pub(crate) fn matches_installation(&self, installation: &PythonInstallation) -> bool {
3095 let request = self.clone().into_request_for_source(installation.source);
3096 request.matches_interpreter(&installation.interpreter)
3097 }
3098
3099 pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
3101 match self {
3102 Self::Any => true,
3103 Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
3105 Self::Major(major, variant) => {
3106 interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
3107 }
3108 Self::MajorMinor(major, minor, variant) => {
3109 (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
3110 && variant.matches_interpreter(interpreter)
3111 }
3112 Self::MajorMinorPatch(major, minor, patch, variant) => {
3113 (
3114 interpreter.python_major(),
3115 interpreter.python_minor(),
3116 interpreter.python_patch(),
3117 ) == (*major, *minor, *patch)
3118 && interpreter.python_version().pre().is_none()
3121 && variant.matches_interpreter(interpreter)
3122 }
3123 Self::Range(specifiers, variant) => {
3124 let version = if specifiers
3127 .iter()
3128 .any(uv_pep440::VersionSpecifier::any_prerelease)
3129 {
3130 Cow::Borrowed(interpreter.python_version())
3131 } else {
3132 Cow::Owned(interpreter.python_version().only_release())
3133 };
3134 specifiers.contains(&version) && variant.matches_interpreter(interpreter)
3135 }
3136 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3137 let version = interpreter.python_version();
3138 let Some(interpreter_prerelease) = version.pre() else {
3139 return false;
3140 };
3141 (
3142 interpreter.python_major(),
3143 interpreter.python_minor(),
3144 interpreter_prerelease,
3145 ) == (*major, *minor, *prerelease)
3146 && variant.matches_interpreter(interpreter)
3147 }
3148 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3149 let version = interpreter.python_version();
3150 let Some(interpreter_prerelease) = version.pre() else {
3151 return false;
3152 };
3153 (
3154 interpreter.python_major(),
3155 interpreter.python_minor(),
3156 interpreter.python_patch(),
3157 interpreter_prerelease,
3158 ) == (*major, *minor, *patch, *prerelease)
3159 && variant.matches_interpreter(interpreter)
3160 }
3161 }
3162 }
3163
3164 fn matches_version(&self, version: &PythonVersion) -> bool {
3169 match self {
3170 Self::Any | Self::Default => true,
3171 Self::Major(major, _) => version.major() == *major,
3172 Self::MajorMinor(major, minor, _) => {
3173 (version.major(), version.minor()) == (*major, *minor)
3174 }
3175 Self::MajorMinorPatch(major, minor, patch, _) => {
3176 (version.major(), version.minor(), version.patch())
3177 == (*major, *minor, Some(*patch))
3178 }
3179 Self::Range(specifiers, _) => {
3180 let version = if specifiers
3183 .iter()
3184 .any(uv_pep440::VersionSpecifier::any_prerelease)
3185 {
3186 Cow::Borrowed(&version.version)
3187 } else {
3188 Cow::Owned(version.version.only_release())
3189 };
3190 specifiers.contains(&version)
3191 }
3192 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3193 (version.major(), version.minor(), version.pre())
3194 == (*major, *minor, Some(*prerelease))
3195 }
3196 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3197 (
3198 version.major(),
3199 version.minor(),
3200 version.patch(),
3201 version.pre(),
3202 ) == (*major, *minor, Some(*patch), Some(*prerelease))
3203 }
3204 }
3205 }
3206
3207 fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
3212 match self {
3213 Self::Any | Self::Default => true,
3214 Self::Major(self_major, _) => *self_major == major,
3215 Self::MajorMinor(self_major, self_minor, _) => {
3216 (*self_major, *self_minor) == (major, minor)
3217 }
3218 Self::MajorMinorPatch(self_major, self_minor, _, _) => {
3219 (*self_major, *self_minor) == (major, minor)
3220 }
3221 Self::Range(specifiers, _) => {
3222 let range = release_specifiers_to_ranges(specifiers.clone());
3223 let Some((lower, upper)) = range.bounding_range() else {
3224 return true;
3225 };
3226 let version = Version::new([u64::from(major), u64::from(minor)]);
3227
3228 let lower = LowerBound::new(lower.cloned());
3229 if !lower.major_minor().contains(&version) {
3230 return false;
3231 }
3232
3233 let upper = UpperBound::new(upper.cloned());
3234 if !upper.major_minor().contains(&version) {
3235 return false;
3236 }
3237
3238 true
3239 }
3240 Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
3241 (*self_major, *self_minor) == (major, minor)
3242 }
3243 Self::MajorMinorPatchPrerelease(self_major, self_minor, _, _, _) => {
3244 (*self_major, *self_minor) == (major, minor)
3245 }
3246 }
3247 }
3248
3249 pub(crate) fn matches_major_minor_patch_prerelease(
3255 &self,
3256 major: u8,
3257 minor: u8,
3258 patch: u8,
3259 prerelease: Option<Prerelease>,
3260 ) -> bool {
3261 match self {
3262 Self::Any | Self::Default => true,
3263 Self::Major(self_major, _) => *self_major == major,
3264 Self::MajorMinor(self_major, self_minor, _) => {
3265 (*self_major, *self_minor) == (major, minor)
3266 }
3267 Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
3268 (*self_major, *self_minor, *self_patch) == (major, minor, patch)
3269 && prerelease.is_none()
3272 }
3273 Self::Range(specifiers, _) => specifiers.contains(
3274 &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
3275 .with_pre(prerelease),
3276 ),
3277 Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3278 (*self_major, *self_minor, 0, Some(*self_prerelease))
3280 == (major, minor, patch, prerelease)
3281 }
3282 Self::MajorMinorPatchPrerelease(
3283 self_major,
3284 self_minor,
3285 self_patch,
3286 self_prerelease,
3287 _,
3288 ) => {
3289 (
3290 *self_major,
3291 *self_minor,
3292 *self_patch,
3293 Some(*self_prerelease),
3294 ) == (major, minor, patch, prerelease)
3295 }
3296 }
3297 }
3298
3299 pub(crate) fn matches_installation_key(&self, key: &PythonInstallationKey) -> bool {
3304 self.matches_major_minor_patch_prerelease(key.major, key.minor, key.patch, key.prerelease())
3305 }
3306
3307 fn has_patch(&self) -> bool {
3309 match self {
3310 Self::Any | Self::Default => false,
3311 Self::Major(..) => false,
3312 Self::MajorMinor(..) => false,
3313 Self::MajorMinorPatch(..) => true,
3314 Self::MajorMinorPrerelease(..) => false,
3315 Self::MajorMinorPatchPrerelease(..) => true,
3316 Self::Range(_, _) => false,
3317 }
3318 }
3319
3320 #[must_use]
3324 fn without_patch(self) -> Self {
3325 match self {
3326 Self::Default => Self::Default,
3327 Self::Any => Self::Any,
3328 Self::Major(major, variant) => Self::Major(major, variant),
3329 Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3330 Self::MajorMinorPatch(major, minor, _, variant) => {
3331 Self::MajorMinor(major, minor, variant)
3332 }
3333 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3334 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3335 }
3336 Self::MajorMinorPatchPrerelease(major, minor, _, prerelease, variant) => {
3337 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3338 }
3339 Self::Range(_, _) => self,
3340 }
3341 }
3342
3343 pub(crate) fn allows_prereleases(&self) -> bool {
3345 match self {
3346 Self::Default => false,
3347 Self::Any => true,
3348 Self::Major(..) => false,
3349 Self::MajorMinor(..) => false,
3350 Self::MajorMinorPatch(..) => false,
3351 Self::MajorMinorPrerelease(..) => true,
3352 Self::MajorMinorPatchPrerelease(..) => true,
3353 Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3354 }
3355 }
3356
3357 pub(crate) fn is_debug(&self) -> bool {
3359 match self {
3360 Self::Any | Self::Default => false,
3361 Self::Major(_, variant)
3362 | Self::MajorMinor(_, _, variant)
3363 | Self::MajorMinorPatch(_, _, _, variant)
3364 | Self::MajorMinorPrerelease(_, _, _, variant)
3365 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3366 | Self::Range(_, variant) => variant.is_debug(),
3367 }
3368 }
3369
3370 fn is_freethreaded(&self) -> bool {
3372 match self {
3373 Self::Any | Self::Default => false,
3374 Self::Major(_, variant)
3375 | Self::MajorMinor(_, _, variant)
3376 | Self::MajorMinorPatch(_, _, _, variant)
3377 | Self::MajorMinorPrerelease(_, _, _, variant)
3378 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3379 | Self::Range(_, variant) => variant.is_freethreaded(),
3380 }
3381 }
3382
3383 pub(crate) fn variant(&self) -> Option<PythonVariant> {
3385 match self {
3386 Self::Any => None,
3387 Self::Default => Some(PythonVariant::Default),
3388 Self::Major(_, variant)
3389 | Self::MajorMinor(_, _, variant)
3390 | Self::MajorMinorPatch(_, _, _, variant)
3391 | Self::MajorMinorPrerelease(_, _, _, variant)
3392 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3393 | Self::Range(_, variant) => Some(*variant),
3394 }
3395 }
3396
3397 fn as_pep440_version(&self) -> Option<Version> {
3401 match self {
3402 Self::Default | Self::Any | Self::Range(_, _) => None,
3403 Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3404 Self::MajorMinor(major, minor, _) => {
3405 Some(Version::new([u64::from(*major), u64::from(*minor)]))
3406 }
3407 Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3408 u64::from(*major),
3409 u64::from(*minor),
3410 u64::from(*patch),
3411 ])),
3412 Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3414 Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3415 ),
3416 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => Some(
3417 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3418 .with_pre(Some(*prerelease)),
3419 ),
3420 }
3421 }
3422
3423 fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
3429 match self {
3430 Self::Default | Self::Any => None,
3431 Self::Major(major, _) => Some(VersionSpecifiers::from(
3432 VersionSpecifier::equals_star_version(Version::new([u64::from(*major)])),
3433 )),
3434 Self::MajorMinor(major, minor, _) => Some(VersionSpecifiers::from(
3435 VersionSpecifier::equals_star_version(Version::new([
3436 u64::from(*major),
3437 u64::from(*minor),
3438 ])),
3439 )),
3440 Self::MajorMinorPatch(major, minor, patch, _) => {
3441 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3442 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)]),
3443 )))
3444 }
3445 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3446 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3447 Version::new([u64::from(*major), u64::from(*minor), 0])
3448 .with_pre(Some(*prerelease)),
3449 )))
3450 }
3451 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3452 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3453 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3454 .with_pre(Some(*prerelease)),
3455 )))
3456 }
3457 Self::Range(specifiers, _) => Some(specifiers.clone()),
3458 }
3459 }
3460}
3461
3462impl FromStr for VersionRequest {
3463 type Err = Error;
3464
3465 fn from_str(s: &str) -> Result<Self, Self::Err> {
3466 fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3469 if s.chars().all(char::is_alphabetic) {
3471 return Err(Error::InvalidVersionRequest(s.to_string()));
3472 }
3473
3474 let Some(mut start) = s.rfind(|c: char| c.is_ascii_digit()) else {
3475 return Ok((s, PythonVariant::Default));
3476 };
3477
3478 start += 1;
3480
3481 if start + 1 > s.len() {
3483 return Ok((s, PythonVariant::Default));
3484 }
3485
3486 let variant = &s[start..];
3487 let prefix = &s[..start];
3488
3489 let variant = variant.strip_prefix('+').unwrap_or(variant);
3491
3492 let Ok(variant) = PythonVariant::from_str(variant) else {
3496 return Ok((s, PythonVariant::Default));
3497 };
3498
3499 Ok((prefix, variant))
3500 }
3501
3502 let (s, variant) = parse_variant(s)?;
3503 let Ok(version) = Version::from_str(s) else {
3504 return parse_version_specifiers_request(s, variant);
3505 };
3506
3507 let version = split_wheel_tag_release_version(version);
3509
3510 if version.post().is_some() || version.dev().is_some() {
3512 return Err(Error::InvalidVersionRequest(s.to_string()));
3513 }
3514
3515 if !version.local().is_empty() {
3518 return Err(Error::InvalidVersionRequest(s.to_string()));
3519 }
3520
3521 let Ok(release) = try_into_u8_slice(&version.release()) else {
3523 return Err(Error::InvalidVersionRequest(s.to_string()));
3524 };
3525
3526 let prerelease = version.pre();
3527
3528 match release.as_slice() {
3529 [major] => {
3531 if prerelease.is_some() {
3533 return Err(Error::InvalidVersionRequest(s.to_string()));
3534 }
3535 Ok(Self::Major(*major, variant))
3536 }
3537 [major, minor] => {
3539 if let Some(prerelease) = prerelease {
3540 return Ok(Self::MajorMinorPrerelease(
3541 *major, *minor, prerelease, variant,
3542 ));
3543 }
3544 Ok(Self::MajorMinor(*major, *minor, variant))
3545 }
3546 [major, minor, patch] => {
3548 if let Some(prerelease) = prerelease {
3549 if *patch == 0 {
3550 return Ok(Self::MajorMinorPrerelease(
3551 *major, *minor, prerelease, variant,
3552 ));
3553 }
3554 return Ok(Self::MajorMinorPatchPrerelease(
3555 *major, *minor, *patch, prerelease, variant,
3556 ));
3557 }
3558 Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3559 }
3560 _ => Err(Error::InvalidVersionRequest(s.to_string())),
3561 }
3562 }
3563}
3564
3565impl FromStr for PythonVariant {
3566 type Err = ();
3567
3568 fn from_str(s: &str) -> Result<Self, Self::Err> {
3569 match s {
3570 "t" | "freethreaded" => Ok(Self::Freethreaded),
3571 "d" | "debug" => Ok(Self::Debug),
3572 "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3573 "gil" => Ok(Self::Gil),
3574 "gil+debug" => Ok(Self::GilDebug),
3575 "" => Ok(Self::Default),
3576 _ => Err(()),
3577 }
3578 }
3579}
3580
3581impl fmt::Display for PythonVariant {
3582 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3583 match self {
3584 Self::Default => f.write_str("default"),
3585 Self::Debug => f.write_str("debug"),
3586 Self::Freethreaded => f.write_str("freethreaded"),
3587 Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3588 Self::Gil => f.write_str("gil"),
3589 Self::GilDebug => f.write_str("gil+debug"),
3590 }
3591 }
3592}
3593
3594fn parse_version_specifiers_request(
3595 s: &str,
3596 variant: PythonVariant,
3597) -> Result<VersionRequest, Error> {
3598 let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3599 return Err(Error::InvalidVersionRequest(s.to_string()));
3600 };
3601 if specifiers.is_empty() {
3602 return Err(Error::InvalidVersionRequest(s.to_string()));
3603 }
3604 Ok(VersionRequest::from_specifiers(specifiers, variant))
3605}
3606
3607impl From<&PythonVersion> for VersionRequest {
3608 fn from(version: &PythonVersion) -> Self {
3609 Self::from_str(&version.string)
3610 .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3611 }
3612}
3613
3614impl fmt::Display for VersionRequest {
3615 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3616 match self {
3617 Self::Any => f.write_str("any"),
3618 Self::Default => f.write_str("default"),
3619 Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3620 Self::MajorMinor(major, minor, variant) => {
3621 write!(f, "{major}.{minor}{}", variant.display_suffix())
3622 }
3623 Self::MajorMinorPatch(major, minor, patch, variant) => {
3624 write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3625 }
3626 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3627 write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3628 }
3629 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3630 write!(
3631 f,
3632 "{major}.{minor}.{patch}{prerelease}{}",
3633 variant.display_suffix()
3634 )
3635 }
3636 Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3637 }
3638 }
3639}
3640
3641impl fmt::Display for PythonRequest {
3642 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3643 match self {
3644 Self::Default => write!(f, "a default Python"),
3645 Self::Any => write!(f, "any Python"),
3646 Self::Version(version) => write!(f, "Python {version}"),
3647 Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3648 Self::File(path) => write!(f, "path `{}`", path.user_display()),
3649 Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3650 Self::Implementation(implementation) => {
3651 write!(f, "{}", implementation.pretty())
3652 }
3653 Self::ImplementationVersion(implementation, version) => {
3654 write!(f, "{} {version}", implementation.pretty())
3655 }
3656 Self::Key(request) => write!(f, "{request}"),
3657 }
3658 }
3659}
3660
3661impl fmt::Display for PythonSource {
3662 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3663 match self {
3664 Self::ProvidedPath => f.write_str("provided path"),
3665 Self::ActiveEnvironment => f.write_str("active virtual environment"),
3666 Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3667 Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3668 Self::SearchPath => f.write_str("search path"),
3669 Self::SearchPathFirst => f.write_str("first executable in the search path"),
3670 Self::Registry => f.write_str("registry"),
3671 Self::MicrosoftStore => f.write_str("Microsoft Store"),
3672 Self::Managed => f.write_str("managed installations"),
3673 Self::ParentInterpreter => f.write_str("parent interpreter"),
3674 }
3675 }
3676}
3677
3678impl PythonPreference {
3679 fn sources(self) -> &'static [PythonSource] {
3682 match self {
3683 Self::OnlyManaged => &[PythonSource::Managed],
3684 Self::Managed => {
3685 if cfg!(windows) {
3686 &[
3687 PythonSource::Managed,
3688 PythonSource::SearchPath,
3689 PythonSource::Registry,
3690 ]
3691 } else {
3692 &[PythonSource::Managed, PythonSource::SearchPath]
3693 }
3694 }
3695 Self::System => {
3696 if cfg!(windows) {
3697 &[
3698 PythonSource::SearchPath,
3699 PythonSource::Registry,
3700 PythonSource::Managed,
3701 ]
3702 } else {
3703 &[PythonSource::SearchPath, PythonSource::Managed]
3704 }
3705 }
3706 Self::OnlySystem => {
3707 if cfg!(windows) {
3708 &[PythonSource::SearchPath, PythonSource::Registry]
3709 } else {
3710 &[PythonSource::SearchPath]
3711 }
3712 }
3713 }
3714 }
3715}
3716
3717impl fmt::Display for PythonPreference {
3718 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3719 f.write_str(match self {
3720 Self::OnlyManaged => "only managed",
3721 Self::Managed => "prefer managed",
3722 Self::System => "prefer system",
3723 Self::OnlySystem => "only system",
3724 })
3725 }
3726}
3727
3728impl DiscoveryPreferences {
3729 fn sources(&self, request: &PythonRequest) -> String {
3732 let python_sources = self
3733 .python_preference
3734 .sources()
3735 .iter()
3736 .map(ToString::to_string)
3737 .collect::<Vec<_>>();
3738 match self.environment_preference {
3739 EnvironmentPreference::Any => disjunction(
3740 &["virtual environments"]
3741 .into_iter()
3742 .chain(python_sources.iter().map(String::as_str))
3743 .collect::<Vec<_>>(),
3744 ),
3745 EnvironmentPreference::ExplicitSystem => {
3746 if request.is_explicit_system() {
3747 disjunction(
3748 &["virtual environments"]
3749 .into_iter()
3750 .chain(python_sources.iter().map(String::as_str))
3751 .collect::<Vec<_>>(),
3752 )
3753 } else {
3754 disjunction(&["virtual environments"])
3755 }
3756 }
3757 EnvironmentPreference::OnlySystem => disjunction(
3758 &python_sources
3759 .iter()
3760 .map(String::as_str)
3761 .collect::<Vec<_>>(),
3762 ),
3763 EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3764 }
3765 }
3766}
3767
3768impl fmt::Display for PythonNotFound {
3769 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3770 let sources = DiscoveryPreferences {
3771 python_preference: self.python_preference,
3772 environment_preference: self.environment_preference,
3773 }
3774 .sources(&self.request);
3775
3776 match self.request {
3777 PythonRequest::Default | PythonRequest::Any => {
3778 write!(f, "No interpreter found in {sources}")
3779 }
3780 PythonRequest::File(_) => {
3781 write!(f, "No interpreter found at {}", self.request)
3782 }
3783 PythonRequest::Directory(_) => {
3784 write!(f, "No interpreter found in {}", self.request)
3785 }
3786 _ => {
3787 write!(f, "No interpreter found for {} in {sources}", self.request)
3788 }
3789 }
3790 }
3791}
3792
3793fn disjunction(items: &[&str]) -> String {
3795 match items.len() {
3796 0 => String::new(),
3797 1 => items[0].to_string(),
3798 2 => format!("{} or {}", items[0], items[1]),
3799 _ => {
3800 let last = items.last().unwrap();
3801 format!(
3802 "{}, or {}",
3803 items.iter().take(items.len() - 1).join(", "),
3804 last
3805 )
3806 }
3807 }
3808}
3809
3810fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3811 release
3812 .iter()
3813 .map(|x| match u8::try_from(*x) {
3814 Ok(x) => Ok(x),
3815 Err(e) => Err(e),
3816 })
3817 .collect()
3818}
3819
3820fn split_wheel_tag_release_version(version: Version) -> Version {
3827 let release = version.release();
3828 if release.len() != 1 {
3829 return version;
3830 }
3831
3832 let release = release[0].to_string();
3833 let mut chars = release.chars();
3834 let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3835 return version;
3836 };
3837
3838 let Ok(minor) = chars.as_str().parse::<u32>() else {
3839 return version;
3840 };
3841
3842 version.with_release([u64::from(major), u64::from(minor)])
3843}
3844
3845#[cfg(test)]
3846mod tests {
3847 use std::assert_matches;
3848 use std::{cell::Cell, io, path::PathBuf, str::FromStr};
3849
3850 use assert_fs::{TempDir, prelude::*};
3851 use target_lexicon::{Aarch64Architecture, Architecture};
3852 use test_log::test;
3853 use uv_cache::Cache;
3854 use uv_distribution_types::RequiresPython;
3855 use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3856
3857 use crate::{
3858 discovery::{PythonRequest, VersionRequest},
3859 downloads::{ArchRequest, PythonDownloadRequest},
3860 implementation::ImplementationName,
3861 };
3862 use uv_platform::{Arch, Libc, Os};
3863
3864 use super::{
3865 DiscoveryPreferences, EnvironmentPreference, Error, InterpreterError,
3866 PythonExecutableGroup, PythonPreference, PythonSource, PythonVariant, QueryStrategy,
3867 python_installations_from_executables, sort_installations_by_key,
3868 };
3869
3870 #[test]
3872 fn installation_key_order_only_partitions_critical_errors() {
3873 let query_error = |error| {
3874 Error::Query(
3875 Box::new(error),
3876 PathBuf::from("python"),
3877 PythonSource::SearchPath,
3878 )
3879 };
3880
3881 let mut installations = [
3882 Ok(1_u8),
3883 Err(query_error(InterpreterError::NotFound(PathBuf::from(
3884 "missing",
3885 )))),
3886 Ok(2),
3887 Err(query_error(InterpreterError::Io(io::Error::other(
3888 "critical",
3889 )))),
3890 Ok(3),
3891 ];
3892
3893 sort_installations_by_key(&mut installations, |key| *key);
3894
3895 assert_matches!(
3896 &installations[..],
3897 [Ok(2), Ok(1), Err(noncritical), Err(critical), Ok(3)]
3898 if !noncritical.is_critical() && critical.is_critical()
3899 );
3900 }
3901
3902 #[test]
3903 fn sequential_query_strategy_does_not_prefetch_executable_groups() -> anyhow::Result<()> {
3904 let cache = Cache::temp()?;
3905 let pulls = Cell::new(0);
3906 let executables = (0..2).map(|_| {
3907 pulls.set(pulls.get() + 1);
3908 Err::<PythonExecutableGroup, _>(Error::SourceNotAllowed(
3909 PythonRequest::Default,
3910 PythonSource::SearchPath,
3911 PythonPreference::OnlyManaged,
3912 ))
3913 });
3914
3915 let mut installations =
3916 python_installations_from_executables(executables, &cache, QueryStrategy::Sequential);
3917
3918 assert_eq!(pulls.get(), 0);
3919 assert!(installations.next().is_some_and(|result| result.is_err()));
3920 assert_eq!(pulls.get(), 1);
3921
3922 Ok(())
3923 }
3924
3925 #[test]
3926 fn interpreter_request_from_str() {
3927 assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3928 assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
3929 assert_eq!(
3930 PythonRequest::parse("3.12"),
3931 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
3932 );
3933 assert_eq!(
3934 PythonRequest::parse(">=3.12"),
3935 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3936 );
3937 assert_eq!(
3938 PythonRequest::parse(">=3.12,<3.13"),
3939 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3940 );
3941 assert_eq!(
3942 PythonRequest::parse(">=3.12,<3.13"),
3943 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3944 );
3945
3946 assert_eq!(
3947 PythonRequest::parse("3.13.0a1"),
3948 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3949 );
3950 assert_eq!(
3951 PythonRequest::parse("3.13.0b5"),
3952 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3953 );
3954 assert_eq!(
3955 PythonRequest::parse("3.13.0rc1"),
3956 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3957 );
3958 assert_eq!(
3959 PythonRequest::parse("3.13.1rc1"),
3960 PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3961 "Pre-release version requests require a patch version of zero"
3962 );
3963 assert_eq!(
3964 PythonRequest::parse("3rc1"),
3965 PythonRequest::ExecutableName("3rc1".to_string()),
3966 "Pre-release version requests require a minor version"
3967 );
3968
3969 assert_eq!(
3970 PythonRequest::parse("cpython"),
3971 PythonRequest::Implementation(ImplementationName::CPython)
3972 );
3973
3974 assert_eq!(
3975 PythonRequest::parse("cpython3.12.2"),
3976 PythonRequest::ImplementationVersion(
3977 ImplementationName::CPython,
3978 VersionRequest::from_str("3.12.2").unwrap(),
3979 )
3980 );
3981
3982 assert_eq!(
3983 PythonRequest::parse("cpython-3.13.2"),
3984 PythonRequest::Key(PythonDownloadRequest {
3985 version: Some(VersionRequest::MajorMinorPatch(
3986 3,
3987 13,
3988 2,
3989 PythonVariant::Default
3990 )),
3991 implementation: Some(ImplementationName::CPython),
3992 arch: None,
3993 os: None,
3994 libc: None,
3995 build: None,
3996 prereleases: None
3997 })
3998 );
3999 assert_eq!(
4000 PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
4001 PythonRequest::Key(PythonDownloadRequest {
4002 version: Some(VersionRequest::MajorMinorPatch(
4003 3,
4004 13,
4005 2,
4006 PythonVariant::Default
4007 )),
4008 implementation: Some(ImplementationName::CPython),
4009 arch: Some(ArchRequest::Explicit(Arch::new(
4010 Architecture::Aarch64(Aarch64Architecture::Aarch64),
4011 None
4012 ))),
4013 os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
4014 libc: Some(Libc::None),
4015 build: None,
4016 prereleases: None
4017 })
4018 );
4019 assert_eq!(
4020 PythonRequest::parse("any-3.13.2"),
4021 PythonRequest::Key(PythonDownloadRequest {
4022 version: Some(VersionRequest::MajorMinorPatch(
4023 3,
4024 13,
4025 2,
4026 PythonVariant::Default
4027 )),
4028 implementation: None,
4029 arch: None,
4030 os: None,
4031 libc: None,
4032 build: None,
4033 prereleases: None
4034 })
4035 );
4036 assert_eq!(
4037 PythonRequest::parse("any-3.13.2-any-aarch64"),
4038 PythonRequest::Key(PythonDownloadRequest {
4039 version: Some(VersionRequest::MajorMinorPatch(
4040 3,
4041 13,
4042 2,
4043 PythonVariant::Default
4044 )),
4045 implementation: None,
4046 arch: Some(ArchRequest::Explicit(Arch::new(
4047 Architecture::Aarch64(Aarch64Architecture::Aarch64),
4048 None
4049 ))),
4050 os: None,
4051 libc: None,
4052 build: None,
4053 prereleases: None
4054 })
4055 );
4056
4057 assert_eq!(
4058 PythonRequest::parse("pypy"),
4059 PythonRequest::Implementation(ImplementationName::PyPy)
4060 );
4061 assert_eq!(
4062 PythonRequest::parse("pp"),
4063 PythonRequest::Implementation(ImplementationName::PyPy)
4064 );
4065 assert_eq!(
4066 PythonRequest::parse("graalpy"),
4067 PythonRequest::Implementation(ImplementationName::GraalPy)
4068 );
4069 assert_eq!(
4070 PythonRequest::parse("gp"),
4071 PythonRequest::Implementation(ImplementationName::GraalPy)
4072 );
4073 assert_eq!(
4074 PythonRequest::parse("cp"),
4075 PythonRequest::Implementation(ImplementationName::CPython)
4076 );
4077 assert_eq!(
4078 PythonRequest::parse("pypy3.10"),
4079 PythonRequest::ImplementationVersion(
4080 ImplementationName::PyPy,
4081 VersionRequest::from_str("3.10").unwrap(),
4082 )
4083 );
4084 assert_eq!(
4085 PythonRequest::parse("pp310"),
4086 PythonRequest::ImplementationVersion(
4087 ImplementationName::PyPy,
4088 VersionRequest::from_str("3.10").unwrap(),
4089 )
4090 );
4091 assert_eq!(
4092 PythonRequest::parse("graalpy3.10"),
4093 PythonRequest::ImplementationVersion(
4094 ImplementationName::GraalPy,
4095 VersionRequest::from_str("3.10").unwrap(),
4096 )
4097 );
4098 assert_eq!(
4099 PythonRequest::parse("gp310"),
4100 PythonRequest::ImplementationVersion(
4101 ImplementationName::GraalPy,
4102 VersionRequest::from_str("3.10").unwrap(),
4103 )
4104 );
4105 assert_eq!(
4106 PythonRequest::parse("cp38"),
4107 PythonRequest::ImplementationVersion(
4108 ImplementationName::CPython,
4109 VersionRequest::from_str("3.8").unwrap(),
4110 )
4111 );
4112 assert_eq!(
4113 PythonRequest::parse("pypy@3.10"),
4114 PythonRequest::ImplementationVersion(
4115 ImplementationName::PyPy,
4116 VersionRequest::from_str("3.10").unwrap(),
4117 )
4118 );
4119 assert_eq!(
4120 PythonRequest::parse("pypy310"),
4121 PythonRequest::ImplementationVersion(
4122 ImplementationName::PyPy,
4123 VersionRequest::from_str("3.10").unwrap(),
4124 )
4125 );
4126 assert_eq!(
4127 PythonRequest::parse("graalpy@3.10"),
4128 PythonRequest::ImplementationVersion(
4129 ImplementationName::GraalPy,
4130 VersionRequest::from_str("3.10").unwrap(),
4131 )
4132 );
4133 assert_eq!(
4134 PythonRequest::parse("graalpy310"),
4135 PythonRequest::ImplementationVersion(
4136 ImplementationName::GraalPy,
4137 VersionRequest::from_str("3.10").unwrap(),
4138 )
4139 );
4140
4141 let tempdir = TempDir::new().unwrap();
4142 assert_eq!(
4143 PythonRequest::parse(tempdir.path().to_str().unwrap()),
4144 PythonRequest::Directory(tempdir.path().to_path_buf()),
4145 "An existing directory is treated as a directory"
4146 );
4147 assert_eq!(
4148 PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
4149 PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
4150 "A path that does not exist is treated as a file"
4151 );
4152 tempdir.child("bar").touch().unwrap();
4153 assert_eq!(
4154 PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
4155 PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
4156 "An existing file is treated as a file"
4157 );
4158 assert_eq!(
4159 PythonRequest::parse("./foo"),
4160 PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
4161 "A string with a file system separator is treated as a file"
4162 );
4163 assert_eq!(
4164 PythonRequest::parse("3.13t"),
4165 PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
4166 );
4167 }
4168
4169 #[test]
4170 fn discovery_sources_prefer_system_orders_search_path_first() {
4171 let preferences = DiscoveryPreferences {
4172 python_preference: PythonPreference::System,
4173 environment_preference: EnvironmentPreference::OnlySystem,
4174 };
4175 let sources = preferences.sources(&PythonRequest::Default);
4176
4177 if cfg!(windows) {
4178 assert_eq!(sources, "search path, registry, or managed installations");
4179 } else {
4180 assert_eq!(sources, "search path or managed installations");
4181 }
4182 }
4183
4184 #[test]
4185 fn discovery_sources_only_system_matches_platform_order() {
4186 let preferences = DiscoveryPreferences {
4187 python_preference: PythonPreference::OnlySystem,
4188 environment_preference: EnvironmentPreference::OnlySystem,
4189 };
4190 let sources = preferences.sources(&PythonRequest::Default);
4191
4192 if cfg!(windows) {
4193 assert_eq!(sources, "search path or registry");
4194 } else {
4195 assert_eq!(sources, "search path");
4196 }
4197 }
4198
4199 #[test]
4200 fn interpreter_request_to_canonical_string() {
4201 assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
4202 assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
4203 assert_eq!(
4204 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
4205 "3.12"
4206 );
4207 assert_eq!(
4208 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
4209 .to_canonical_string(),
4210 ">=3.12"
4211 );
4212 assert_eq!(
4213 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
4214 .to_canonical_string(),
4215 ">=3.12, <3.13"
4216 );
4217
4218 assert_eq!(
4219 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
4220 .to_canonical_string(),
4221 "3.13a1"
4222 );
4223
4224 assert_eq!(
4225 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
4226 .to_canonical_string(),
4227 "3.13b5"
4228 );
4229
4230 assert_eq!(
4231 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
4232 .to_canonical_string(),
4233 "3.13rc1"
4234 );
4235
4236 assert_eq!(
4237 PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
4238 .to_canonical_string(),
4239 "3.13rc4"
4240 );
4241
4242 assert_eq!(
4243 PythonRequest::Version(VersionRequest::from_str("3.14.5rc1").unwrap())
4244 .to_canonical_string(),
4245 "3.14.5rc1"
4246 );
4247
4248 assert_eq!(
4249 PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
4250 "foo"
4251 );
4252 assert_eq!(
4253 PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
4254 "cpython"
4255 );
4256 assert_eq!(
4257 PythonRequest::ImplementationVersion(
4258 ImplementationName::CPython,
4259 VersionRequest::from_str("3.12.2").unwrap(),
4260 )
4261 .to_canonical_string(),
4262 "cpython@3.12.2"
4263 );
4264 assert_eq!(
4265 PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
4266 "pypy"
4267 );
4268 assert_eq!(
4269 PythonRequest::ImplementationVersion(
4270 ImplementationName::PyPy,
4271 VersionRequest::from_str("3.10").unwrap(),
4272 )
4273 .to_canonical_string(),
4274 "pypy@3.10"
4275 );
4276 assert_eq!(
4277 PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
4278 "graalpy"
4279 );
4280 assert_eq!(
4281 PythonRequest::ImplementationVersion(
4282 ImplementationName::GraalPy,
4283 VersionRequest::from_str("3.10").unwrap(),
4284 )
4285 .to_canonical_string(),
4286 "graalpy@3.10"
4287 );
4288
4289 let tempdir = TempDir::new().unwrap();
4290 assert_eq!(
4291 PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
4292 tempdir.path().to_str().unwrap(),
4293 "An existing directory is treated as a directory"
4294 );
4295 assert_eq!(
4296 PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
4297 tempdir.child("foo").path().to_str().unwrap(),
4298 "A path that does not exist is treated as a file"
4299 );
4300 tempdir.child("bar").touch().unwrap();
4301 assert_eq!(
4302 PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
4303 tempdir.child("bar").path().to_str().unwrap(),
4304 "An existing file is treated as a file"
4305 );
4306 assert_eq!(
4307 PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
4308 "./foo",
4309 "A string with a file system separator is treated as a file"
4310 );
4311 }
4312
4313 #[test]
4314 fn version_request_from_str() {
4315 assert_eq!(
4316 VersionRequest::from_str("3").unwrap(),
4317 VersionRequest::Major(3, PythonVariant::Default)
4318 );
4319 assert_eq!(
4320 VersionRequest::from_str("3.12").unwrap(),
4321 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4322 );
4323 assert_eq!(
4324 VersionRequest::from_str("3.12.1").unwrap(),
4325 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4326 );
4327 assert!(VersionRequest::from_str("1.foo.1").is_err());
4328 assert_eq!(
4329 VersionRequest::from_str("3").unwrap(),
4330 VersionRequest::Major(3, PythonVariant::Default)
4331 );
4332 assert_eq!(
4333 VersionRequest::from_str("38").unwrap(),
4334 VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
4335 );
4336 assert_eq!(
4337 VersionRequest::from_str("312").unwrap(),
4338 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4339 );
4340 assert_eq!(
4341 VersionRequest::from_str("3100").unwrap(),
4342 VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
4343 );
4344 assert_eq!(
4345 VersionRequest::from_str("3.13a1").unwrap(),
4346 VersionRequest::MajorMinorPrerelease(
4347 3,
4348 13,
4349 Prerelease {
4350 kind: PrereleaseKind::Alpha,
4351 number: 1
4352 },
4353 PythonVariant::Default
4354 )
4355 );
4356 assert_eq!(
4357 VersionRequest::from_str("313b1").unwrap(),
4358 VersionRequest::MajorMinorPrerelease(
4359 3,
4360 13,
4361 Prerelease {
4362 kind: PrereleaseKind::Beta,
4363 number: 1
4364 },
4365 PythonVariant::Default
4366 )
4367 );
4368 assert_eq!(
4369 VersionRequest::from_str("3.13.0b2").unwrap(),
4370 VersionRequest::MajorMinorPrerelease(
4371 3,
4372 13,
4373 Prerelease {
4374 kind: PrereleaseKind::Beta,
4375 number: 2
4376 },
4377 PythonVariant::Default
4378 )
4379 );
4380 assert_eq!(
4381 VersionRequest::from_str("3.13.0rc3").unwrap(),
4382 VersionRequest::MajorMinorPrerelease(
4383 3,
4384 13,
4385 Prerelease {
4386 kind: PrereleaseKind::Rc,
4387 number: 3
4388 },
4389 PythonVariant::Default
4390 )
4391 );
4392 assert_matches!(
4393 VersionRequest::from_str("3rc1"),
4394 Err(Error::InvalidVersionRequest(_)),
4395 "Pre-release version requests require a minor version"
4396 );
4397 assert_eq!(
4398 VersionRequest::from_str("3.14.5rc1").unwrap(),
4399 VersionRequest::MajorMinorPatchPrerelease(
4400 3,
4401 14,
4402 5,
4403 Prerelease {
4404 kind: PrereleaseKind::Rc,
4405 number: 1
4406 },
4407 PythonVariant::Default
4408 ),
4409 "Pre-release version requests with a non-zero patch are allowed (e.g., `3.14.5rc1`)"
4410 );
4411 assert_eq!(
4412 VersionRequest::from_str("3.13.2rc1").unwrap(),
4413 VersionRequest::MajorMinorPatchPrerelease(
4414 3,
4415 13,
4416 2,
4417 Prerelease {
4418 kind: PrereleaseKind::Rc,
4419 number: 1
4420 },
4421 PythonVariant::Default
4422 )
4423 );
4424 assert_matches!(
4425 VersionRequest::from_str("3.12-dev"),
4426 Err(Error::InvalidVersionRequest(_)),
4427 "Development version segments are not allowed"
4428 );
4429 assert_matches!(
4430 VersionRequest::from_str("3.12+local"),
4431 Err(Error::InvalidVersionRequest(_)),
4432 "Local version segments are not allowed"
4433 );
4434 assert_matches!(
4435 VersionRequest::from_str("3.12.post0"),
4436 Err(Error::InvalidVersionRequest(_)),
4437 "Post version segments are not allowed"
4438 );
4439 assert!(
4440 matches!(
4442 VersionRequest::from_str("31000"),
4443 Err(Error::InvalidVersionRequest(_))
4444 )
4445 );
4446 assert_eq!(
4447 VersionRequest::from_str("3t").unwrap(),
4448 VersionRequest::Major(3, PythonVariant::Freethreaded)
4449 );
4450 assert_eq!(
4451 VersionRequest::from_str("313t").unwrap(),
4452 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4453 );
4454 assert_eq!(
4455 VersionRequest::from_str("3.13t").unwrap(),
4456 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4457 );
4458 assert_eq!(
4459 VersionRequest::from_str(">=3.13t").unwrap(),
4460 VersionRequest::Range(
4461 VersionSpecifiers::from_str(">=3.13").unwrap(),
4462 PythonVariant::Freethreaded
4463 )
4464 );
4465 assert_eq!(
4466 VersionRequest::from_str(">=3.13").unwrap(),
4467 VersionRequest::Range(
4468 VersionSpecifiers::from_str(">=3.13").unwrap(),
4469 PythonVariant::Default
4470 )
4471 );
4472 assert_eq!(
4473 VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4474 VersionRequest::Range(
4475 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4476 PythonVariant::Freethreaded
4477 )
4478 );
4479 assert_matches!(
4480 VersionRequest::from_str("3.13tt"),
4481 Err(Error::InvalidVersionRequest(_))
4482 );
4483 assert_matches!(
4484 VersionRequest::from_str("3.12²t"),
4485 Err(Error::InvalidVersionRequest(_))
4486 );
4487
4488 assert_eq!(
4490 VersionRequest::from_str("==3.12").unwrap(),
4491 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4492 );
4493 assert_eq!(
4494 VersionRequest::from_str("==3.12.1").unwrap(),
4495 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4496 );
4497 }
4498
4499 #[test]
4500 fn version_request_from_specifiers() {
4501 assert_eq!(
4503 VersionRequest::from_specifiers(
4504 VersionSpecifiers::from_str("==3.12").unwrap(),
4505 PythonVariant::Default
4506 ),
4507 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4508 );
4509 assert_eq!(
4510 VersionRequest::from_specifiers(
4511 VersionSpecifiers::from_str("==3.12.1").unwrap(),
4512 PythonVariant::Default
4513 ),
4514 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4515 );
4516
4517 assert_eq!(
4519 VersionRequest::from_specifiers(
4520 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4521 PythonVariant::Default
4522 ),
4523 VersionRequest::Range(
4524 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4525 PythonVariant::Default
4526 )
4527 );
4528
4529 assert_eq!(
4531 VersionRequest::from_specifiers(
4532 VersionSpecifiers::from_str(">=3.12").unwrap(),
4533 PythonVariant::Default
4534 ),
4535 VersionRequest::Range(
4536 VersionSpecifiers::from_str(">=3.12").unwrap(),
4537 PythonVariant::Default
4538 )
4539 );
4540
4541 assert_eq!(
4543 VersionRequest::from_specifiers(
4544 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4545 PythonVariant::Default
4546 ),
4547 VersionRequest::Range(
4548 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4549 PythonVariant::Default
4550 )
4551 );
4552 }
4553
4554 #[test]
4555 fn executable_names_from_request() {
4556 fn case(request: &str, expected: &[&str]) {
4557 let (implementation, version) = match PythonRequest::parse(request) {
4558 PythonRequest::Any => (None, VersionRequest::Any),
4559 PythonRequest::Default => (None, VersionRequest::Default),
4560 PythonRequest::Version(version) => (None, version),
4561 PythonRequest::ImplementationVersion(implementation, version) => {
4562 (Some(implementation), version)
4563 }
4564 PythonRequest::Implementation(implementation) => {
4565 (Some(implementation), VersionRequest::Default)
4566 }
4567 result => {
4568 panic!("Test cases should request versions or implementations; got {result:?}")
4569 }
4570 };
4571
4572 let result: Vec<_> = version
4573 .executable_names(implementation.as_ref())
4574 .into_iter()
4575 .map(|name| name.to_string())
4576 .collect();
4577
4578 let expected: Vec<_> = expected
4579 .iter()
4580 .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4581 .collect();
4582
4583 assert_eq!(result, expected, "mismatch for case \"{request}\"");
4584 }
4585
4586 case(
4587 "any",
4588 &[
4589 "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4590 "pyodide", "pyodide3",
4591 ],
4592 );
4593
4594 case("default", &["python", "python3"]);
4595
4596 case("3", &["python3", "python"]);
4597
4598 case("4", &["python4", "python"]);
4599
4600 case("3.13", &["python3.13", "python3", "python"]);
4601
4602 case("pypy", &["pypy", "pypy3", "python", "python3"]);
4603
4604 case(
4605 "pypy@3.10",
4606 &[
4607 "pypy3.10",
4608 "pypy3",
4609 "pypy",
4610 "python3.10",
4611 "python3",
4612 "python",
4613 ],
4614 );
4615
4616 case(
4617 "3.13t",
4618 &[
4619 "python3.13t",
4620 "python3.13",
4621 "python3t",
4622 "python3",
4623 "pythont",
4624 "python",
4625 ],
4626 );
4627 case("3t", &["python3t", "python3", "pythont", "python"]);
4628
4629 case(
4630 "3.13.2",
4631 &["python3.13.2", "python3.13", "python3", "python"],
4632 );
4633
4634 case(
4635 "3.13rc2",
4636 &["python3.13rc2", "python3.13", "python3", "python"],
4637 );
4638 }
4639
4640 #[test]
4641 fn test_try_split_prefix_and_version() {
4642 assert_matches!(
4643 PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4644 Ok(None),
4645 );
4646 assert_matches!(
4647 PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4648 Ok(Some(_)),
4649 );
4650 assert_matches!(
4651 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4652 Ok(Some(_)),
4653 );
4654 assert_matches!(
4655 PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4656 Ok(None),
4657 );
4658 assert!(
4660 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4661 );
4662 assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4664 }
4665
4666 #[test]
4667 fn version_request_as_pep440_version() {
4668 assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4670 assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4671 assert_eq!(
4672 VersionRequest::from_str(">=3.10")
4673 .unwrap()
4674 .as_pep440_version(),
4675 None
4676 );
4677
4678 assert_eq!(
4680 VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4681 Some(Version::from_str("3").unwrap())
4682 );
4683
4684 assert_eq!(
4686 VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4687 Some(Version::from_str("3.12").unwrap())
4688 );
4689
4690 assert_eq!(
4692 VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4693 Some(Version::from_str("3.12.5").unwrap())
4694 );
4695
4696 assert_eq!(
4698 VersionRequest::MajorMinorPrerelease(
4699 3,
4700 14,
4701 Prerelease {
4702 kind: PrereleaseKind::Alpha,
4703 number: 1
4704 },
4705 PythonVariant::Default
4706 )
4707 .as_pep440_version(),
4708 Some(Version::from_str("3.14.0a1").unwrap())
4709 );
4710 assert_eq!(
4711 VersionRequest::MajorMinorPrerelease(
4712 3,
4713 14,
4714 Prerelease {
4715 kind: PrereleaseKind::Beta,
4716 number: 2
4717 },
4718 PythonVariant::Default
4719 )
4720 .as_pep440_version(),
4721 Some(Version::from_str("3.14.0b2").unwrap())
4722 );
4723 assert_eq!(
4724 VersionRequest::MajorMinorPrerelease(
4725 3,
4726 13,
4727 Prerelease {
4728 kind: PrereleaseKind::Rc,
4729 number: 3
4730 },
4731 PythonVariant::Default
4732 )
4733 .as_pep440_version(),
4734 Some(Version::from_str("3.13.0rc3").unwrap())
4735 );
4736
4737 assert_eq!(
4739 VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4740 Some(Version::from_str("3").unwrap())
4741 );
4742 assert_eq!(
4743 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4744 Some(Version::from_str("3.13").unwrap())
4745 );
4746 }
4747
4748 #[test]
4749 fn python_request_as_pep440_version() {
4750 assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4752 assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4753
4754 assert_eq!(
4756 PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4757 .as_pep440_version(),
4758 Some(Version::from_str("3.11").unwrap())
4759 );
4760
4761 assert_eq!(
4763 PythonRequest::ImplementationVersion(
4764 ImplementationName::CPython,
4765 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4766 )
4767 .as_pep440_version(),
4768 Some(Version::from_str("3.12.1").unwrap())
4769 );
4770
4771 assert_eq!(
4773 PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4774 None
4775 );
4776
4777 assert_eq!(
4779 PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4780 Some(Version::from_str("3.13.2").unwrap())
4781 );
4782
4783 assert_eq!(
4785 PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4786 None
4787 );
4788
4789 assert_eq!(
4791 PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4792 None
4793 );
4794 }
4795
4796 #[test]
4797 fn intersects_requires_python_exact() {
4798 let requires_python =
4799 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.12").unwrap());
4800
4801 assert!(PythonRequest::parse("3.12").intersects_requires_python(&requires_python));
4802 assert!(!PythonRequest::parse("3.11").intersects_requires_python(&requires_python));
4803 }
4804
4805 #[test]
4806 fn intersects_requires_python_major() {
4807 let requires_python =
4808 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.12").unwrap());
4809
4810 assert!(PythonRequest::parse("3").intersects_requires_python(&requires_python));
4812 assert!(!PythonRequest::parse("2").intersects_requires_python(&requires_python));
4814 }
4815
4816 #[test]
4817 fn intersects_requires_python_range() {
4818 let requires_python =
4819 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.12").unwrap());
4820
4821 assert!(PythonRequest::parse(">=3.12,<3.13").intersects_requires_python(&requires_python));
4822 assert!(!PythonRequest::parse(">=3.10,<3.12").intersects_requires_python(&requires_python));
4823 }
4824
4825 #[test]
4826 fn intersects_requires_python_implementation_range() {
4827 let requires_python =
4828 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.12").unwrap());
4829
4830 assert!(
4831 PythonRequest::parse("cpython@>=3.12,<3.13")
4832 .intersects_requires_python(&requires_python)
4833 );
4834 assert!(
4835 !PythonRequest::parse("cpython@>=3.10,<3.12")
4836 .intersects_requires_python(&requires_python)
4837 );
4838 }
4839
4840 #[test]
4841 fn intersects_requires_python_no_version() {
4842 let requires_python =
4843 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.12").unwrap());
4844
4845 assert!(PythonRequest::Any.intersects_requires_python(&requires_python));
4847 assert!(PythonRequest::Default.intersects_requires_python(&requires_python));
4848 assert!(
4849 PythonRequest::Implementation(ImplementationName::CPython)
4850 .intersects_requires_python(&requires_python)
4851 );
4852 }
4853}