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