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