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