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