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 && let Err(err) = version.check_supported()
1221 {
1222 return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
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 && let Err(err) = result
1297 {
1298 first_error = Some(err);
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 = ImplementationName::iter_all().flat_map(|implementation| {
1844 std::iter::once(implementation.long_name()).chain(implementation.short_name())
1845 });
1846 if let Ok(Some(request)) = Self::parse_versions_and_implementations(
1849 abstract_version_prefixes,
1850 all_implementation_names,
1851 lowercase_value,
1852 ) {
1853 return request;
1854 }
1855
1856 let value_as_path = PathBuf::from(value);
1857 if value_as_path.is_dir() {
1859 return Self::Directory(value_as_path);
1860 }
1861 if value_as_path.is_file() {
1863 return Self::File(value_as_path);
1864 }
1865
1866 #[cfg(windows)]
1868 if value_as_path.extension().is_none() {
1869 let value_as_path = value_as_path.with_extension(EXE_SUFFIX);
1870 if value_as_path.is_file() {
1871 return Self::File(value_as_path);
1872 }
1873 }
1874
1875 #[cfg(test)]
1880 if value_as_path.is_relative() {
1881 if let Ok(current_dir) = crate::current_dir() {
1882 let relative = current_dir.join(&value_as_path);
1883 if relative.is_dir() {
1884 return Self::Directory(relative);
1885 }
1886 if relative.is_file() {
1887 return Self::File(relative);
1888 }
1889 }
1890 }
1891 if value.contains(std::path::MAIN_SEPARATOR) {
1894 return Self::File(value_as_path);
1895 }
1896 if cfg!(windows) && value.contains('/') {
1899 return Self::File(value_as_path);
1900 }
1901 if let Ok(request) = PythonDownloadRequest::from_str(value) {
1902 return Self::Key(request);
1903 }
1904 Self::ExecutableName(value.to_string())
1907 }
1908
1909 pub fn try_from_tool_name(value: &str) -> Result<Option<Self>, Error> {
1923 let lowercase_value = &value.to_ascii_lowercase();
1924 let abstract_version_prefixes = if cfg!(windows) {
1926 &["python", "pythonw"][..]
1927 } else {
1928 &["python"][..]
1929 };
1930 if abstract_version_prefixes.contains(&lowercase_value.as_str()) {
1932 return Ok(Some(Self::Default));
1933 }
1934 Self::parse_versions_and_implementations(
1935 abstract_version_prefixes.iter().copied(),
1936 ImplementationName::iter_all().map(ImplementationName::long_name),
1937 lowercase_value,
1938 )
1939 }
1940
1941 fn parse_versions_and_implementations<'a>(
1950 abstract_version_prefixes: impl IntoIterator<Item = &'a str>,
1952 implementation_names: impl IntoIterator<Item = &'a str>,
1954 lowercase_value: &str,
1956 ) -> Result<Option<Self>, Error> {
1957 for prefix in abstract_version_prefixes {
1958 if let Some(version_request) =
1959 Self::try_split_prefix_and_version(prefix, lowercase_value)?
1960 {
1961 return Ok(Some(Self::Version(version_request)));
1965 }
1966 }
1967 for implementation in implementation_names {
1968 if lowercase_value == implementation {
1969 return Ok(Some(Self::Implementation(
1970 ImplementationName::from_str(implementation).unwrap(),
1973 )));
1974 }
1975 if let Some(version_request) =
1976 Self::try_split_prefix_and_version(implementation, lowercase_value)?
1977 {
1978 return Ok(Some(Self::ImplementationVersion(
1980 ImplementationName::from_str(implementation).unwrap(),
1982 version_request,
1983 )));
1984 }
1985 }
1986 Ok(None)
1987 }
1988
1989 fn try_split_prefix_and_version(
2000 prefix: &str,
2001 lowercase_value: &str,
2002 ) -> Result<Option<VersionRequest>, Error> {
2003 if lowercase_value.starts_with('@') {
2004 return Err(Error::InvalidVersionRequest(lowercase_value.to_string()));
2005 }
2006 let Some(rest) = lowercase_value.strip_prefix(prefix) else {
2007 return Ok(None);
2008 };
2009 if rest.is_empty() {
2011 return Ok(None);
2012 }
2013 if let Some(after_at) = rest.strip_prefix('@') {
2016 if after_at == "latest" {
2017 return Err(Error::LatestVersionRequest);
2020 }
2021 return after_at.parse().map(Some);
2022 }
2023 Ok(rest.parse().ok())
2026 }
2027
2028 pub fn includes_patch(&self) -> bool {
2030 match self {
2031 Self::Default => false,
2032 Self::Any => false,
2033 Self::Version(version_request) => version_request.patch().is_some(),
2034 Self::Directory(..) => false,
2035 Self::File(..) => false,
2036 Self::ExecutableName(..) => false,
2037 Self::Implementation(..) => false,
2038 Self::ImplementationVersion(_, version) => version.patch().is_some(),
2039 Self::Key(request) => request
2040 .version
2041 .as_ref()
2042 .is_some_and(|request| request.patch().is_some()),
2043 }
2044 }
2045
2046 pub fn includes_prerelease(&self) -> bool {
2048 match self {
2049 Self::Default => false,
2050 Self::Any => false,
2051 Self::Version(version_request) => version_request.prerelease().is_some(),
2052 Self::Directory(..) => false,
2053 Self::File(..) => false,
2054 Self::ExecutableName(..) => false,
2055 Self::Implementation(..) => false,
2056 Self::ImplementationVersion(_, version) => version.prerelease().is_some(),
2057 Self::Key(request) => request
2058 .version
2059 .as_ref()
2060 .is_some_and(|request| request.prerelease().is_some()),
2061 }
2062 }
2063
2064 pub fn satisfied(&self, interpreter: &Interpreter, cache: &Cache) -> bool {
2066 fn is_same_executable(path1: &Path, path2: &Path) -> bool {
2068 path1 == path2 || is_same_file(path1, path2).unwrap_or(false)
2069 }
2070
2071 match self {
2072 Self::Default | Self::Any => true,
2073 Self::Version(version_request) => version_request.matches_interpreter(interpreter),
2074 Self::Directory(directory) => {
2075 is_same_executable(directory, interpreter.sys_prefix())
2077 || is_same_executable(
2078 virtualenv_python_executable(directory).as_path(),
2079 interpreter.sys_executable(),
2080 )
2081 }
2082 Self::File(file) => {
2083 if is_same_executable(interpreter.sys_executable(), file) {
2085 return true;
2086 }
2087 if interpreter
2089 .sys_base_executable()
2090 .is_some_and(|sys_base_executable| {
2091 is_same_executable(sys_base_executable, file)
2092 })
2093 {
2094 return true;
2095 }
2096 if cfg!(windows) {
2101 if let Ok(file_interpreter) = Interpreter::query(file, cache) {
2102 if let (Some(file_base), Some(interpreter_base)) = (
2103 file_interpreter.sys_base_executable(),
2104 interpreter.sys_base_executable(),
2105 ) {
2106 if is_same_executable(file_base, interpreter_base) {
2107 return true;
2108 }
2109 }
2110 }
2111 }
2112 false
2113 }
2114 Self::ExecutableName(name) => {
2115 if interpreter
2117 .sys_executable()
2118 .file_name()
2119 .is_some_and(|filename| filename == name.as_str())
2120 {
2121 return true;
2122 }
2123 if interpreter
2125 .sys_base_executable()
2126 .and_then(|executable| executable.file_name())
2127 .is_some_and(|file_name| file_name == name.as_str())
2128 {
2129 return true;
2130 }
2131 if which(name)
2134 .ok()
2135 .as_ref()
2136 .and_then(|executable| executable.file_name())
2137 .is_some_and(|file_name| file_name == name.as_str())
2138 {
2139 return true;
2140 }
2141 false
2142 }
2143 Self::Implementation(implementation) => interpreter
2144 .implementation_name()
2145 .eq_ignore_ascii_case(implementation.long_name()),
2146 Self::ImplementationVersion(implementation, version) => {
2147 version.matches_interpreter(interpreter)
2148 && interpreter
2149 .implementation_name()
2150 .eq_ignore_ascii_case(implementation.long_name())
2151 }
2152 Self::Key(request) => request.satisfied_by_interpreter(interpreter),
2153 }
2154 }
2155
2156 pub(crate) fn allows_prereleases(&self) -> bool {
2158 match self {
2159 Self::Default => false,
2160 Self::Any => true,
2161 Self::Version(version) => version.allows_prereleases(),
2162 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2163 Self::Implementation(_) => false,
2164 Self::ImplementationVersion(_, _) => true,
2165 Self::Key(request) => request.allows_prereleases(),
2166 }
2167 }
2168
2169 fn allows_debug(&self) -> bool {
2171 match self {
2172 Self::Default => false,
2173 Self::Any => true,
2174 Self::Version(version) => version.is_debug(),
2175 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2176 Self::Implementation(_) => false,
2177 Self::ImplementationVersion(_, _) => true,
2178 Self::Key(request) => request.allows_debug(),
2179 }
2180 }
2181
2182 fn allows_alternative_implementations(&self) -> bool {
2184 match self {
2185 Self::Default => false,
2186 Self::Any => true,
2187 Self::Version(_) => false,
2188 Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
2189 Self::Implementation(implementation)
2190 | Self::ImplementationVersion(implementation, _) => {
2191 !matches!(implementation, ImplementationName::CPython)
2192 }
2193 Self::Key(request) => request.allows_alternative_implementations(),
2194 }
2195 }
2196
2197 pub(crate) fn is_explicit_system(&self) -> bool {
2198 matches!(self, Self::File(_) | Self::Directory(_))
2199 }
2200
2201 pub fn to_canonical_string(&self) -> String {
2205 match self {
2206 Self::Any => "any".to_string(),
2207 Self::Default => "default".to_string(),
2208 Self::Version(version) => version.to_string(),
2209 Self::Directory(path) => path.display().to_string(),
2210 Self::File(path) => path.display().to_string(),
2211 Self::ExecutableName(name) => name.clone(),
2212 Self::Implementation(implementation) => implementation.to_string(),
2213 Self::ImplementationVersion(implementation, version) => {
2214 format!("{implementation}@{version}")
2215 }
2216 Self::Key(request) => request.to_string(),
2217 }
2218 }
2219
2220 pub fn as_pep440_version(&self) -> Option<Version> {
2224 match self {
2225 Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2226 Self::Key(download_request) => download_request
2227 .version()
2228 .and_then(VersionRequest::as_pep440_version),
2229 Self::Default
2230 | Self::Any
2231 | Self::Directory(_)
2232 | Self::File(_)
2233 | Self::ExecutableName(_)
2234 | Self::Implementation(_) => None,
2235 }
2236 }
2237
2238 fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2244 match self {
2245 Self::Version(version) | Self::ImplementationVersion(_, version) => {
2246 version.as_version_specifiers()
2247 }
2248 Self::Key(download_request) => download_request
2249 .version()
2250 .and_then(VersionRequest::as_version_specifiers),
2251 Self::Default
2252 | Self::Any
2253 | Self::Directory(_)
2254 | Self::File(_)
2255 | Self::ExecutableName(_)
2256 | Self::Implementation(_) => None,
2257 }
2258 }
2259
2260 pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2266 let Some(specifiers) = self.as_version_specifiers() else {
2267 return true;
2268 };
2269
2270 let request_range = release_specifiers_to_ranges(specifiers);
2271 let requires_python_range =
2272 release_specifiers_to_ranges(requires_python.specifiers().clone());
2273 !request_range
2274 .intersection(&requires_python_range)
2275 .is_empty()
2276 }
2277}
2278
2279impl PythonSource {
2280 pub fn is_managed(self) -> bool {
2281 matches!(self, Self::Managed)
2282 }
2283
2284 fn allows_prereleases(self) -> bool {
2286 match self {
2287 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2288 Self::SearchPath
2289 | Self::SearchPathFirst
2290 | Self::CondaPrefix
2291 | Self::BaseCondaPrefix
2292 | Self::ProvidedPath
2293 | Self::ParentInterpreter
2294 | Self::ActiveEnvironment
2295 | Self::DiscoveredEnvironment => true,
2296 }
2297 }
2298
2299 fn allows_debug(self) -> bool {
2301 match self {
2302 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2303 Self::SearchPath
2304 | Self::SearchPathFirst
2305 | Self::CondaPrefix
2306 | Self::BaseCondaPrefix
2307 | Self::ProvidedPath
2308 | Self::ParentInterpreter
2309 | Self::ActiveEnvironment
2310 | Self::DiscoveredEnvironment => true,
2311 }
2312 }
2313
2314 fn allows_alternative_implementations(self) -> bool {
2316 match self {
2317 Self::Managed
2318 | Self::Registry
2319 | Self::SearchPath
2320 | Self::SearchPathFirst
2323 | Self::MicrosoftStore => false,
2324 Self::CondaPrefix
2325 | Self::BaseCondaPrefix
2326 | Self::ProvidedPath
2327 | Self::ParentInterpreter
2328 | Self::ActiveEnvironment
2329 | Self::DiscoveredEnvironment => true,
2330 }
2331 }
2332
2333 fn is_maybe_virtualenv(self) -> bool {
2345 match self {
2346 Self::ProvidedPath
2347 | Self::ActiveEnvironment
2348 | Self::DiscoveredEnvironment
2349 | Self::CondaPrefix
2350 | Self::BaseCondaPrefix
2351 | Self::ParentInterpreter
2352 | Self::SearchPathFirst => true,
2353 Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2354 }
2355 }
2356
2357 fn is_explicit(self) -> bool {
2360 match self {
2361 Self::ProvidedPath
2362 | Self::ParentInterpreter
2363 | Self::ActiveEnvironment
2364 | Self::CondaPrefix => true,
2365 Self::Managed
2366 | Self::DiscoveredEnvironment
2367 | Self::SearchPath
2368 | Self::SearchPathFirst
2369 | Self::Registry
2370 | Self::MicrosoftStore
2371 | Self::BaseCondaPrefix => false,
2372 }
2373 }
2374
2375 fn is_maybe_system(self) -> bool {
2377 match self {
2378 Self::CondaPrefix
2379 | Self::BaseCondaPrefix
2380 | Self::ParentInterpreter
2381 | Self::ProvidedPath
2382 | Self::Managed
2383 | Self::SearchPath
2384 | Self::SearchPathFirst
2385 | Self::Registry
2386 | Self::MicrosoftStore => true,
2387 Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2388 }
2389 }
2390}
2391
2392impl PythonPreference {
2393 fn allows_source(self, source: PythonSource) -> bool {
2394 if !matches!(
2396 source,
2397 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2398 ) {
2399 return true;
2400 }
2401
2402 match self {
2403 Self::OnlyManaged => matches!(source, PythonSource::Managed),
2404 Self::Managed | Self::System => matches!(
2405 source,
2406 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2407 ),
2408 Self::OnlySystem => {
2409 matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2410 }
2411 }
2412 }
2413
2414 pub(crate) fn allows_managed(self) -> bool {
2415 match self {
2416 Self::OnlySystem => false,
2417 Self::Managed | Self::System | Self::OnlyManaged => true,
2418 }
2419 }
2420
2421 fn allows_interpreter(self, interpreter: &Interpreter) -> bool {
2426 match self {
2427 Self::OnlyManaged => interpreter.is_managed(),
2428 Self::OnlySystem => !interpreter.is_managed(),
2429 Self::Managed | Self::System => true,
2430 }
2431 }
2432
2433 pub fn allows_installation(self, installation: &PythonInstallation) -> bool {
2441 let source = installation.source;
2442 let interpreter = &installation.interpreter;
2443
2444 match self {
2445 Self::OnlyManaged => {
2446 if self.allows_interpreter(interpreter) {
2447 true
2448 } else if source.is_explicit() {
2449 debug!(
2450 "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2451 interpreter.sys_executable().display()
2452 );
2453 true
2454 } else {
2455 debug!(
2456 "Ignoring Python interpreter at `{}`: only managed interpreters allowed",
2457 interpreter.sys_executable().display()
2458 );
2459 false
2460 }
2461 }
2462 Self::Managed | Self::System => true,
2464 Self::OnlySystem => {
2465 if self.allows_interpreter(interpreter) {
2466 true
2467 } else if source.is_explicit() {
2468 debug!(
2469 "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2470 interpreter.sys_executable().display()
2471 );
2472 true
2473 } else {
2474 debug!(
2475 "Ignoring Python interpreter at `{}`: only system interpreters allowed",
2476 interpreter.sys_executable().display()
2477 );
2478 false
2479 }
2480 }
2481 }
2482 }
2483
2484 #[must_use]
2489 pub fn with_system_flag(self, system: bool) -> Self {
2490 match self {
2491 Self::OnlyManaged => self,
2496 Self::Managed => {
2497 if system {
2498 Self::System
2499 } else {
2500 self
2501 }
2502 }
2503 Self::System => self,
2504 Self::OnlySystem => self,
2505 }
2506 }
2507}
2508
2509impl PythonDownloads {
2510 pub fn is_automatic(self) -> bool {
2511 matches!(self, Self::Automatic)
2512 }
2513}
2514
2515impl EnvironmentPreference {
2516 pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2517 match (system, mutable) {
2518 (true, _) => Self::OnlySystem,
2520 (false, true) => Self::ExplicitSystem,
2522 (false, false) => Self::Any,
2524 }
2525 }
2526
2527 pub(crate) fn allows_installation(self, installation: &PythonInstallation) -> bool {
2533 interpreter_satisfies_environment_preference(
2534 installation.source,
2535 &installation.interpreter,
2536 self,
2537 )
2538 }
2539}
2540
2541#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2542pub(crate) struct ExecutableName {
2543 implementation: Option<ImplementationName>,
2544 major: Option<u8>,
2545 minor: Option<u8>,
2546 patch: Option<u8>,
2547 prerelease: Option<Prerelease>,
2548 variant: PythonVariant,
2549}
2550
2551#[derive(Debug, Clone, PartialEq, Eq)]
2552struct ExecutableNameComparator<'a> {
2553 name: ExecutableName,
2554 request: &'a VersionRequest,
2555 implementation: Option<&'a ImplementationName>,
2556}
2557
2558impl Ord for ExecutableNameComparator<'_> {
2559 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2563 let name_ordering = if self.implementation.is_some() {
2566 std::cmp::Ordering::Greater
2567 } else {
2568 std::cmp::Ordering::Less
2569 };
2570 if self.name.implementation.is_none() && other.name.implementation.is_some() {
2571 return name_ordering.reverse();
2572 }
2573 if self.name.implementation.is_some() && other.name.implementation.is_none() {
2574 return name_ordering;
2575 }
2576 let ordering = self.name.implementation.cmp(&other.name.implementation);
2578 if ordering != std::cmp::Ordering::Equal {
2579 return ordering;
2580 }
2581 let ordering = self.name.major.cmp(&other.name.major);
2582 let is_default_request =
2583 matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2584 if ordering != std::cmp::Ordering::Equal {
2585 return if is_default_request {
2586 ordering.reverse()
2587 } else {
2588 ordering
2589 };
2590 }
2591 let ordering = self.name.minor.cmp(&other.name.minor);
2592 if ordering != std::cmp::Ordering::Equal {
2593 return if is_default_request {
2594 ordering.reverse()
2595 } else {
2596 ordering
2597 };
2598 }
2599 let ordering = self.name.patch.cmp(&other.name.patch);
2600 if ordering != std::cmp::Ordering::Equal {
2601 return if is_default_request {
2602 ordering.reverse()
2603 } else {
2604 ordering
2605 };
2606 }
2607 let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2608 if ordering != std::cmp::Ordering::Equal {
2609 return if is_default_request {
2610 ordering.reverse()
2611 } else {
2612 ordering
2613 };
2614 }
2615 let ordering = self.name.variant.cmp(&other.name.variant);
2616 if ordering != std::cmp::Ordering::Equal {
2617 return if is_default_request {
2618 ordering.reverse()
2619 } else {
2620 ordering
2621 };
2622 }
2623 ordering
2624 }
2625}
2626
2627impl PartialOrd for ExecutableNameComparator<'_> {
2628 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2629 Some(self.cmp(other))
2630 }
2631}
2632
2633impl ExecutableName {
2634 #[must_use]
2635 fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2636 self.implementation = Some(implementation);
2637 self
2638 }
2639
2640 #[must_use]
2641 fn with_major(mut self, major: u8) -> Self {
2642 self.major = Some(major);
2643 self
2644 }
2645
2646 #[must_use]
2647 fn with_minor(mut self, minor: u8) -> Self {
2648 self.minor = Some(minor);
2649 self
2650 }
2651
2652 #[must_use]
2653 fn with_patch(mut self, patch: u8) -> Self {
2654 self.patch = Some(patch);
2655 self
2656 }
2657
2658 #[must_use]
2659 fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2660 self.prerelease = Some(prerelease);
2661 self
2662 }
2663
2664 #[must_use]
2665 fn with_variant(mut self, variant: PythonVariant) -> Self {
2666 self.variant = variant;
2667 self
2668 }
2669
2670 fn into_comparator<'a>(
2671 self,
2672 request: &'a VersionRequest,
2673 implementation: Option<&'a ImplementationName>,
2674 ) -> ExecutableNameComparator<'a> {
2675 ExecutableNameComparator {
2676 name: self,
2677 request,
2678 implementation,
2679 }
2680 }
2681}
2682
2683impl fmt::Display for ExecutableName {
2684 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2685 if let Some(implementation) = self.implementation {
2686 write!(f, "{implementation}")?;
2687 } else {
2688 f.write_str("python")?;
2689 }
2690 if let Some(major) = self.major {
2691 write!(f, "{major}")?;
2692 if let Some(minor) = self.minor {
2693 write!(f, ".{minor}")?;
2694 if let Some(patch) = self.patch {
2695 write!(f, ".{patch}")?;
2696 }
2697 }
2698 }
2699 if let Some(prerelease) = &self.prerelease {
2700 write!(f, "{prerelease}")?;
2701 }
2702 f.write_str(self.variant.executable_suffix())?;
2703 f.write_str(EXE_SUFFIX)?;
2704 Ok(())
2705 }
2706}
2707
2708impl VersionRequest {
2709 pub fn from_specifiers(specifiers: VersionSpecifiers, variant: PythonVariant) -> Self {
2714 if let [specifier] = specifiers.iter().as_slice()
2715 && specifier.operator() == &uv_pep440::Operator::Equal
2716 && let Ok(request) = Self::from_str(&specifier.version().to_string())
2717 {
2718 return request;
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 && variant != PythonVariant::Default
2824 {
2825 for i in 0..names.len() {
2826 let name = names[i].with_variant(variant);
2827 names.push(name);
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 && let Self::MajorMinor(major, minor, _) = self.clone().without_patch()
2932 && (major, minor) < (3, 13)
2933 {
2934 return Err(format!(
2935 "Python <3.13 does not support free-threading but {self} was requested."
2936 ));
2937 }
2938
2939 Ok(())
2940 }
2941
2942 #[must_use]
2948 fn into_request_for_source(self, source: PythonSource) -> Self {
2949 match self {
2950 Self::Default => match source {
2951 PythonSource::ParentInterpreter
2952 | PythonSource::CondaPrefix
2953 | PythonSource::BaseCondaPrefix
2954 | PythonSource::ProvidedPath
2955 | PythonSource::DiscoveredEnvironment
2956 | PythonSource::ActiveEnvironment => Self::Any,
2957 PythonSource::SearchPath
2958 | PythonSource::SearchPathFirst
2959 | PythonSource::Registry
2960 | PythonSource::MicrosoftStore
2961 | PythonSource::Managed => Self::Default,
2962 },
2963 _ => self,
2964 }
2965 }
2966
2967 pub(crate) fn matches_installation(&self, installation: &PythonInstallation) -> bool {
2970 let request = self.clone().into_request_for_source(installation.source);
2971 request.matches_interpreter(&installation.interpreter)
2972 }
2973
2974 pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
2976 match self {
2977 Self::Any => true,
2978 Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
2980 Self::Major(major, variant) => {
2981 interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
2982 }
2983 Self::MajorMinor(major, minor, variant) => {
2984 (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
2985 && variant.matches_interpreter(interpreter)
2986 }
2987 Self::MajorMinorPatch(major, minor, patch, variant) => {
2988 (
2989 interpreter.python_major(),
2990 interpreter.python_minor(),
2991 interpreter.python_patch(),
2992 ) == (*major, *minor, *patch)
2993 && interpreter.python_version().pre().is_none()
2996 && variant.matches_interpreter(interpreter)
2997 }
2998 Self::Range(specifiers, variant) => {
2999 let version = if specifiers
3002 .iter()
3003 .any(uv_pep440::VersionSpecifier::any_prerelease)
3004 {
3005 Cow::Borrowed(interpreter.python_version())
3006 } else {
3007 Cow::Owned(interpreter.python_version().only_release())
3008 };
3009 specifiers.contains(&version) && variant.matches_interpreter(interpreter)
3010 }
3011 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3012 let version = interpreter.python_version();
3013 let Some(interpreter_prerelease) = version.pre() else {
3014 return false;
3015 };
3016 (
3017 interpreter.python_major(),
3018 interpreter.python_minor(),
3019 interpreter_prerelease,
3020 ) == (*major, *minor, *prerelease)
3021 && variant.matches_interpreter(interpreter)
3022 }
3023 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3024 let version = interpreter.python_version();
3025 let Some(interpreter_prerelease) = version.pre() else {
3026 return false;
3027 };
3028 (
3029 interpreter.python_major(),
3030 interpreter.python_minor(),
3031 interpreter.python_patch(),
3032 interpreter_prerelease,
3033 ) == (*major, *minor, *patch, *prerelease)
3034 && variant.matches_interpreter(interpreter)
3035 }
3036 }
3037 }
3038
3039 fn matches_version(&self, version: &PythonVersion) -> bool {
3044 match self {
3045 Self::Any | Self::Default => true,
3046 Self::Major(major, _) => version.major() == *major,
3047 Self::MajorMinor(major, minor, _) => {
3048 (version.major(), version.minor()) == (*major, *minor)
3049 }
3050 Self::MajorMinorPatch(major, minor, patch, _) => {
3051 (version.major(), version.minor(), version.patch())
3052 == (*major, *minor, Some(*patch))
3053 }
3054 Self::Range(specifiers, _) => {
3055 let version = if specifiers
3058 .iter()
3059 .any(uv_pep440::VersionSpecifier::any_prerelease)
3060 {
3061 Cow::Borrowed(&version.version)
3062 } else {
3063 Cow::Owned(version.version.only_release())
3064 };
3065 specifiers.contains(&version)
3066 }
3067 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3068 (version.major(), version.minor(), version.pre())
3069 == (*major, *minor, Some(*prerelease))
3070 }
3071 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3072 (
3073 version.major(),
3074 version.minor(),
3075 version.patch(),
3076 version.pre(),
3077 ) == (*major, *minor, Some(*patch), Some(*prerelease))
3078 }
3079 }
3080 }
3081
3082 fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
3087 match self {
3088 Self::Any | Self::Default => true,
3089 Self::Major(self_major, _) => *self_major == major,
3090 Self::MajorMinor(self_major, self_minor, _) => {
3091 (*self_major, *self_minor) == (major, minor)
3092 }
3093 Self::MajorMinorPatch(self_major, self_minor, _, _) => {
3094 (*self_major, *self_minor) == (major, minor)
3095 }
3096 Self::Range(specifiers, _) => {
3097 let range = release_specifiers_to_ranges(specifiers.clone());
3098 let Some((lower, upper)) = range.bounding_range() else {
3099 return true;
3100 };
3101 let version = Version::new([u64::from(major), u64::from(minor)]);
3102
3103 let lower = LowerBound::new(lower.cloned());
3104 if !lower.major_minor().contains(&version) {
3105 return false;
3106 }
3107
3108 let upper = UpperBound::new(upper.cloned());
3109 if !upper.major_minor().contains(&version) {
3110 return false;
3111 }
3112
3113 true
3114 }
3115 Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
3116 (*self_major, *self_minor) == (major, minor)
3117 }
3118 Self::MajorMinorPatchPrerelease(self_major, self_minor, _, _, _) => {
3119 (*self_major, *self_minor) == (major, minor)
3120 }
3121 }
3122 }
3123
3124 pub(crate) fn matches_major_minor_patch_prerelease(
3130 &self,
3131 major: u8,
3132 minor: u8,
3133 patch: u8,
3134 prerelease: Option<Prerelease>,
3135 ) -> bool {
3136 match self {
3137 Self::Any | Self::Default => true,
3138 Self::Major(self_major, _) => *self_major == major,
3139 Self::MajorMinor(self_major, self_minor, _) => {
3140 (*self_major, *self_minor) == (major, minor)
3141 }
3142 Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
3143 (*self_major, *self_minor, *self_patch) == (major, minor, patch)
3144 && prerelease.is_none()
3147 }
3148 Self::Range(specifiers, _) => specifiers.contains(
3149 &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
3150 .with_pre(prerelease),
3151 ),
3152 Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3153 (*self_major, *self_minor, 0, Some(*self_prerelease))
3155 == (major, minor, patch, prerelease)
3156 }
3157 Self::MajorMinorPatchPrerelease(
3158 self_major,
3159 self_minor,
3160 self_patch,
3161 self_prerelease,
3162 _,
3163 ) => {
3164 (
3165 *self_major,
3166 *self_minor,
3167 *self_patch,
3168 Some(*self_prerelease),
3169 ) == (major, minor, patch, prerelease)
3170 }
3171 }
3172 }
3173
3174 pub(crate) fn matches_installation_key(&self, key: &PythonInstallationKey) -> bool {
3179 self.matches_major_minor_patch_prerelease(key.major, key.minor, key.patch, key.prerelease())
3180 }
3181
3182 fn has_patch(&self) -> bool {
3184 match self {
3185 Self::Any | Self::Default => false,
3186 Self::Major(..) => false,
3187 Self::MajorMinor(..) => false,
3188 Self::MajorMinorPatch(..) => true,
3189 Self::MajorMinorPrerelease(..) => false,
3190 Self::MajorMinorPatchPrerelease(..) => true,
3191 Self::Range(_, _) => false,
3192 }
3193 }
3194
3195 #[must_use]
3199 fn without_patch(self) -> Self {
3200 match self {
3201 Self::Default => Self::Default,
3202 Self::Any => Self::Any,
3203 Self::Major(major, variant) => Self::Major(major, variant),
3204 Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3205 Self::MajorMinorPatch(major, minor, _, variant) => {
3206 Self::MajorMinor(major, minor, variant)
3207 }
3208 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3209 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3210 }
3211 Self::MajorMinorPatchPrerelease(major, minor, _, prerelease, variant) => {
3212 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3213 }
3214 Self::Range(_, _) => self,
3215 }
3216 }
3217
3218 pub(crate) fn allows_prereleases(&self) -> bool {
3220 match self {
3221 Self::Default => false,
3222 Self::Any => true,
3223 Self::Major(..) => false,
3224 Self::MajorMinor(..) => false,
3225 Self::MajorMinorPatch(..) => false,
3226 Self::MajorMinorPrerelease(..) => true,
3227 Self::MajorMinorPatchPrerelease(..) => true,
3228 Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3229 }
3230 }
3231
3232 pub(crate) fn is_debug(&self) -> bool {
3234 match self {
3235 Self::Any | Self::Default => false,
3236 Self::Major(_, variant)
3237 | Self::MajorMinor(_, _, variant)
3238 | Self::MajorMinorPatch(_, _, _, variant)
3239 | Self::MajorMinorPrerelease(_, _, _, variant)
3240 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3241 | Self::Range(_, variant) => variant.is_debug(),
3242 }
3243 }
3244
3245 fn is_freethreaded(&self) -> bool {
3247 match self {
3248 Self::Any | Self::Default => false,
3249 Self::Major(_, variant)
3250 | Self::MajorMinor(_, _, variant)
3251 | Self::MajorMinorPatch(_, _, _, variant)
3252 | Self::MajorMinorPrerelease(_, _, _, variant)
3253 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3254 | Self::Range(_, variant) => variant.is_freethreaded(),
3255 }
3256 }
3257
3258 pub(crate) fn variant(&self) -> Option<PythonVariant> {
3260 match self {
3261 Self::Any => None,
3262 Self::Default => Some(PythonVariant::Default),
3263 Self::Major(_, variant)
3264 | Self::MajorMinor(_, _, variant)
3265 | Self::MajorMinorPatch(_, _, _, variant)
3266 | Self::MajorMinorPrerelease(_, _, _, variant)
3267 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3268 | Self::Range(_, variant) => Some(*variant),
3269 }
3270 }
3271
3272 fn as_pep440_version(&self) -> Option<Version> {
3276 match self {
3277 Self::Default | Self::Any | Self::Range(_, _) => None,
3278 Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3279 Self::MajorMinor(major, minor, _) => {
3280 Some(Version::new([u64::from(*major), u64::from(*minor)]))
3281 }
3282 Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3283 u64::from(*major),
3284 u64::from(*minor),
3285 u64::from(*patch),
3286 ])),
3287 Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3289 Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3290 ),
3291 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => Some(
3292 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3293 .with_pre(Some(*prerelease)),
3294 ),
3295 }
3296 }
3297
3298 fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
3304 match self {
3305 Self::Default | Self::Any => None,
3306 Self::Major(major, _) => Some(VersionSpecifiers::from(
3307 VersionSpecifier::equals_star_version(Version::new([u64::from(*major)])),
3308 )),
3309 Self::MajorMinor(major, minor, _) => Some(VersionSpecifiers::from(
3310 VersionSpecifier::equals_star_version(Version::new([
3311 u64::from(*major),
3312 u64::from(*minor),
3313 ])),
3314 )),
3315 Self::MajorMinorPatch(major, minor, patch, _) => {
3316 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3317 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)]),
3318 )))
3319 }
3320 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3321 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3322 Version::new([u64::from(*major), u64::from(*minor), 0])
3323 .with_pre(Some(*prerelease)),
3324 )))
3325 }
3326 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3327 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3328 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3329 .with_pre(Some(*prerelease)),
3330 )))
3331 }
3332 Self::Range(specifiers, _) => Some(specifiers.clone()),
3333 }
3334 }
3335}
3336
3337impl FromStr for VersionRequest {
3338 type Err = Error;
3339
3340 fn from_str(s: &str) -> Result<Self, Self::Err> {
3341 fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3344 if s.chars().all(char::is_alphabetic) {
3346 return Err(Error::InvalidVersionRequest(s.to_string()));
3347 }
3348
3349 let Some(mut start) = s.rfind(|c: char| c.is_ascii_digit()) else {
3350 return Ok((s, PythonVariant::Default));
3351 };
3352
3353 start += 1;
3355
3356 if start + 1 > s.len() {
3358 return Ok((s, PythonVariant::Default));
3359 }
3360
3361 let variant = &s[start..];
3362 let prefix = &s[..start];
3363
3364 let variant = variant.strip_prefix('+').unwrap_or(variant);
3366
3367 let Ok(variant) = PythonVariant::from_str(variant) else {
3371 return Ok((s, PythonVariant::Default));
3372 };
3373
3374 Ok((prefix, variant))
3375 }
3376
3377 let (s, variant) = parse_variant(s)?;
3378 let Ok(version) = Version::from_str(s) else {
3379 return parse_version_specifiers_request(s, variant);
3380 };
3381
3382 let version = split_wheel_tag_release_version(version);
3384
3385 if version.post().is_some() || version.dev().is_some() {
3387 return Err(Error::InvalidVersionRequest(s.to_string()));
3388 }
3389
3390 if !version.local().is_empty() {
3393 return Err(Error::InvalidVersionRequest(s.to_string()));
3394 }
3395
3396 let Ok(release) = try_into_u8_slice(&version.release()) else {
3398 return Err(Error::InvalidVersionRequest(s.to_string()));
3399 };
3400
3401 let prerelease = version.pre();
3402
3403 match release.as_slice() {
3404 [major] => {
3406 if prerelease.is_some() {
3408 return Err(Error::InvalidVersionRequest(s.to_string()));
3409 }
3410 Ok(Self::Major(*major, variant))
3411 }
3412 [major, minor] => {
3414 if let Some(prerelease) = prerelease {
3415 return Ok(Self::MajorMinorPrerelease(
3416 *major, *minor, prerelease, variant,
3417 ));
3418 }
3419 Ok(Self::MajorMinor(*major, *minor, variant))
3420 }
3421 [major, minor, patch] => {
3423 if let Some(prerelease) = prerelease {
3424 if *patch == 0 {
3425 return Ok(Self::MajorMinorPrerelease(
3426 *major, *minor, prerelease, variant,
3427 ));
3428 }
3429 return Ok(Self::MajorMinorPatchPrerelease(
3430 *major, *minor, *patch, prerelease, variant,
3431 ));
3432 }
3433 Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3434 }
3435 _ => Err(Error::InvalidVersionRequest(s.to_string())),
3436 }
3437 }
3438}
3439
3440impl FromStr for PythonVariant {
3441 type Err = ();
3442
3443 fn from_str(s: &str) -> Result<Self, Self::Err> {
3444 match s {
3445 "t" | "freethreaded" => Ok(Self::Freethreaded),
3446 "d" | "debug" => Ok(Self::Debug),
3447 "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3448 "gil" => Ok(Self::Gil),
3449 "gil+debug" => Ok(Self::GilDebug),
3450 "" => Ok(Self::Default),
3451 _ => Err(()),
3452 }
3453 }
3454}
3455
3456impl fmt::Display for PythonVariant {
3457 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3458 match self {
3459 Self::Default => f.write_str("default"),
3460 Self::Debug => f.write_str("debug"),
3461 Self::Freethreaded => f.write_str("freethreaded"),
3462 Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3463 Self::Gil => f.write_str("gil"),
3464 Self::GilDebug => f.write_str("gil+debug"),
3465 }
3466 }
3467}
3468
3469fn parse_version_specifiers_request(
3470 s: &str,
3471 variant: PythonVariant,
3472) -> Result<VersionRequest, Error> {
3473 let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3474 return Err(Error::InvalidVersionRequest(s.to_string()));
3475 };
3476 if specifiers.is_empty() {
3477 return Err(Error::InvalidVersionRequest(s.to_string()));
3478 }
3479 Ok(VersionRequest::from_specifiers(specifiers, variant))
3480}
3481
3482impl From<&PythonVersion> for VersionRequest {
3483 fn from(version: &PythonVersion) -> Self {
3484 Self::from_str(&version.string)
3485 .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3486 }
3487}
3488
3489impl fmt::Display for VersionRequest {
3490 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3491 match self {
3492 Self::Any => f.write_str("any"),
3493 Self::Default => f.write_str("default"),
3494 Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3495 Self::MajorMinor(major, minor, variant) => {
3496 write!(f, "{major}.{minor}{}", variant.display_suffix())
3497 }
3498 Self::MajorMinorPatch(major, minor, patch, variant) => {
3499 write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3500 }
3501 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3502 write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3503 }
3504 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3505 write!(
3506 f,
3507 "{major}.{minor}.{patch}{prerelease}{}",
3508 variant.display_suffix()
3509 )
3510 }
3511 Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3512 }
3513 }
3514}
3515
3516impl fmt::Display for PythonRequest {
3517 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3518 match self {
3519 Self::Default => write!(f, "a default Python"),
3520 Self::Any => write!(f, "any Python"),
3521 Self::Version(version) => write!(f, "Python {version}"),
3522 Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3523 Self::File(path) => write!(f, "path `{}`", path.user_display()),
3524 Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3525 Self::Implementation(implementation) => {
3526 write!(f, "{}", implementation.pretty())
3527 }
3528 Self::ImplementationVersion(implementation, version) => {
3529 write!(f, "{} {version}", implementation.pretty())
3530 }
3531 Self::Key(request) => write!(f, "{request}"),
3532 }
3533 }
3534}
3535
3536impl fmt::Display for PythonSource {
3537 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3538 match self {
3539 Self::ProvidedPath => f.write_str("provided path"),
3540 Self::ActiveEnvironment => f.write_str("active virtual environment"),
3541 Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3542 Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3543 Self::SearchPath => f.write_str("search path"),
3544 Self::SearchPathFirst => f.write_str("first executable in the search path"),
3545 Self::Registry => f.write_str("registry"),
3546 Self::MicrosoftStore => f.write_str("Microsoft Store"),
3547 Self::Managed => f.write_str("managed installations"),
3548 Self::ParentInterpreter => f.write_str("parent interpreter"),
3549 }
3550 }
3551}
3552
3553impl PythonPreference {
3554 fn sources(self) -> &'static [PythonSource] {
3557 match self {
3558 Self::OnlyManaged => &[PythonSource::Managed],
3559 Self::Managed => {
3560 if cfg!(windows) {
3561 &[
3562 PythonSource::Managed,
3563 PythonSource::SearchPath,
3564 PythonSource::Registry,
3565 ]
3566 } else {
3567 &[PythonSource::Managed, PythonSource::SearchPath]
3568 }
3569 }
3570 Self::System => {
3571 if cfg!(windows) {
3572 &[
3573 PythonSource::SearchPath,
3574 PythonSource::Registry,
3575 PythonSource::Managed,
3576 ]
3577 } else {
3578 &[PythonSource::SearchPath, PythonSource::Managed]
3579 }
3580 }
3581 Self::OnlySystem => {
3582 if cfg!(windows) {
3583 &[PythonSource::SearchPath, PythonSource::Registry]
3584 } else {
3585 &[PythonSource::SearchPath]
3586 }
3587 }
3588 }
3589 }
3590}
3591
3592impl fmt::Display for PythonPreference {
3593 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3594 f.write_str(match self {
3595 Self::OnlyManaged => "only managed",
3596 Self::Managed => "prefer managed",
3597 Self::System => "prefer system",
3598 Self::OnlySystem => "only system",
3599 })
3600 }
3601}
3602
3603impl DiscoveryPreferences {
3604 fn sources(&self, request: &PythonRequest) -> String {
3607 let python_sources = self
3608 .python_preference
3609 .sources()
3610 .iter()
3611 .map(ToString::to_string)
3612 .collect::<Vec<_>>();
3613 match self.environment_preference {
3614 EnvironmentPreference::Any => disjunction(
3615 &["virtual environments"]
3616 .into_iter()
3617 .chain(python_sources.iter().map(String::as_str))
3618 .collect::<Vec<_>>(),
3619 ),
3620 EnvironmentPreference::ExplicitSystem => {
3621 if request.is_explicit_system() {
3622 disjunction(
3623 &["virtual environments"]
3624 .into_iter()
3625 .chain(python_sources.iter().map(String::as_str))
3626 .collect::<Vec<_>>(),
3627 )
3628 } else {
3629 disjunction(&["virtual environments"])
3630 }
3631 }
3632 EnvironmentPreference::OnlySystem => disjunction(
3633 &python_sources
3634 .iter()
3635 .map(String::as_str)
3636 .collect::<Vec<_>>(),
3637 ),
3638 EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3639 }
3640 }
3641}
3642
3643impl fmt::Display for PythonNotFound {
3644 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3645 let sources = DiscoveryPreferences {
3646 python_preference: self.python_preference,
3647 environment_preference: self.environment_preference,
3648 }
3649 .sources(&self.request);
3650
3651 match self.request {
3652 PythonRequest::Default | PythonRequest::Any => {
3653 write!(f, "No interpreter found in {sources}")
3654 }
3655 PythonRequest::File(_) => {
3656 write!(f, "No interpreter found at {}", self.request)
3657 }
3658 PythonRequest::Directory(_) => {
3659 write!(f, "No interpreter found in {}", self.request)
3660 }
3661 _ => {
3662 write!(f, "No interpreter found for {} in {sources}", self.request)
3663 }
3664 }
3665 }
3666}
3667
3668fn disjunction(items: &[&str]) -> String {
3670 match items.len() {
3671 0 => String::new(),
3672 1 => items[0].to_string(),
3673 2 => format!("{} or {}", items[0], items[1]),
3674 _ => {
3675 let last = items.last().unwrap();
3676 format!(
3677 "{}, or {}",
3678 items.iter().take(items.len() - 1).join(", "),
3679 last
3680 )
3681 }
3682 }
3683}
3684
3685fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3686 release
3687 .iter()
3688 .map(|x| match u8::try_from(*x) {
3689 Ok(x) => Ok(x),
3690 Err(e) => Err(e),
3691 })
3692 .collect()
3693}
3694
3695fn split_wheel_tag_release_version(version: Version) -> Version {
3702 let release = version.release();
3703 if release.len() != 1 {
3704 return version;
3705 }
3706
3707 let release = release[0].to_string();
3708 let mut chars = release.chars();
3709 let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3710 return version;
3711 };
3712
3713 let Ok(minor) = chars.as_str().parse::<u32>() else {
3714 return version;
3715 };
3716
3717 version.with_release([u64::from(major), u64::from(minor)])
3718}
3719
3720#[cfg(test)]
3721mod tests {
3722 use std::{cell::Cell, path::PathBuf, str::FromStr};
3723
3724 use assert_fs::{TempDir, prelude::*};
3725 use target_lexicon::{Aarch64Architecture, Architecture};
3726 use test_log::test;
3727 use uv_cache::Cache;
3728 use uv_distribution_types::RequiresPython;
3729 use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3730
3731 use crate::{
3732 discovery::{PythonRequest, VersionRequest},
3733 downloads::{ArchRequest, PythonDownloadRequest},
3734 implementation::ImplementationName,
3735 };
3736 use uv_platform::{Arch, Libc, Os};
3737
3738 use super::{
3739 DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonSource,
3740 PythonVariant, QueryStrategy, python_installations_from_executables,
3741 };
3742
3743 #[test]
3744 fn sequential_query_strategy_does_not_prefetch_executables() -> anyhow::Result<()> {
3745 let cache = Cache::temp()?;
3746 let pulls = Cell::new(0);
3747 let executables = (0..2).map(|_| {
3748 pulls.set(pulls.get() + 1);
3749 Err::<(PythonSource, PathBuf), _>(Error::SourceNotAllowed(
3750 PythonRequest::Default,
3751 PythonSource::SearchPath,
3752 PythonPreference::OnlyManaged,
3753 ))
3754 });
3755
3756 let mut installations =
3757 python_installations_from_executables(executables, &cache, QueryStrategy::Sequential);
3758
3759 assert_eq!(pulls.get(), 0);
3760 assert!(installations.next().is_some_and(|result| result.is_err()));
3761 assert_eq!(pulls.get(), 1);
3762
3763 Ok(())
3764 }
3765
3766 #[test]
3767 fn interpreter_request_from_str() {
3768 assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3769 assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
3770 assert_eq!(
3771 PythonRequest::parse("3.12"),
3772 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
3773 );
3774 assert_eq!(
3775 PythonRequest::parse(">=3.12"),
3776 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3777 );
3778 assert_eq!(
3779 PythonRequest::parse(">=3.12,<3.13"),
3780 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3781 );
3782 assert_eq!(
3783 PythonRequest::parse(">=3.12,<3.13"),
3784 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3785 );
3786
3787 assert_eq!(
3788 PythonRequest::parse("3.13.0a1"),
3789 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3790 );
3791 assert_eq!(
3792 PythonRequest::parse("3.13.0b5"),
3793 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3794 );
3795 assert_eq!(
3796 PythonRequest::parse("3.13.0rc1"),
3797 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3798 );
3799 assert_eq!(
3800 PythonRequest::parse("3.13.1rc1"),
3801 PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3802 "Pre-release version requests require a patch version of zero"
3803 );
3804 assert_eq!(
3805 PythonRequest::parse("3rc1"),
3806 PythonRequest::ExecutableName("3rc1".to_string()),
3807 "Pre-release version requests require a minor version"
3808 );
3809
3810 assert_eq!(
3811 PythonRequest::parse("cpython"),
3812 PythonRequest::Implementation(ImplementationName::CPython)
3813 );
3814
3815 assert_eq!(
3816 PythonRequest::parse("cpython3.12.2"),
3817 PythonRequest::ImplementationVersion(
3818 ImplementationName::CPython,
3819 VersionRequest::from_str("3.12.2").unwrap(),
3820 )
3821 );
3822
3823 assert_eq!(
3824 PythonRequest::parse("cpython-3.13.2"),
3825 PythonRequest::Key(PythonDownloadRequest {
3826 version: Some(VersionRequest::MajorMinorPatch(
3827 3,
3828 13,
3829 2,
3830 PythonVariant::Default
3831 )),
3832 implementation: Some(ImplementationName::CPython),
3833 arch: None,
3834 os: None,
3835 libc: None,
3836 build: None,
3837 prereleases: None
3838 })
3839 );
3840 assert_eq!(
3841 PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3842 PythonRequest::Key(PythonDownloadRequest {
3843 version: Some(VersionRequest::MajorMinorPatch(
3844 3,
3845 13,
3846 2,
3847 PythonVariant::Default
3848 )),
3849 implementation: Some(ImplementationName::CPython),
3850 arch: Some(ArchRequest::Explicit(Arch::new(
3851 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3852 None
3853 ))),
3854 os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3855 libc: Some(Libc::None),
3856 build: None,
3857 prereleases: None
3858 })
3859 );
3860 assert_eq!(
3861 PythonRequest::parse("any-3.13.2"),
3862 PythonRequest::Key(PythonDownloadRequest {
3863 version: Some(VersionRequest::MajorMinorPatch(
3864 3,
3865 13,
3866 2,
3867 PythonVariant::Default
3868 )),
3869 implementation: None,
3870 arch: None,
3871 os: None,
3872 libc: None,
3873 build: None,
3874 prereleases: None
3875 })
3876 );
3877 assert_eq!(
3878 PythonRequest::parse("any-3.13.2-any-aarch64"),
3879 PythonRequest::Key(PythonDownloadRequest {
3880 version: Some(VersionRequest::MajorMinorPatch(
3881 3,
3882 13,
3883 2,
3884 PythonVariant::Default
3885 )),
3886 implementation: None,
3887 arch: Some(ArchRequest::Explicit(Arch::new(
3888 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3889 None
3890 ))),
3891 os: None,
3892 libc: None,
3893 build: None,
3894 prereleases: None
3895 })
3896 );
3897
3898 assert_eq!(
3899 PythonRequest::parse("pypy"),
3900 PythonRequest::Implementation(ImplementationName::PyPy)
3901 );
3902 assert_eq!(
3903 PythonRequest::parse("pp"),
3904 PythonRequest::Implementation(ImplementationName::PyPy)
3905 );
3906 assert_eq!(
3907 PythonRequest::parse("graalpy"),
3908 PythonRequest::Implementation(ImplementationName::GraalPy)
3909 );
3910 assert_eq!(
3911 PythonRequest::parse("gp"),
3912 PythonRequest::Implementation(ImplementationName::GraalPy)
3913 );
3914 assert_eq!(
3915 PythonRequest::parse("cp"),
3916 PythonRequest::Implementation(ImplementationName::CPython)
3917 );
3918 assert_eq!(
3919 PythonRequest::parse("pypy3.10"),
3920 PythonRequest::ImplementationVersion(
3921 ImplementationName::PyPy,
3922 VersionRequest::from_str("3.10").unwrap(),
3923 )
3924 );
3925 assert_eq!(
3926 PythonRequest::parse("pp310"),
3927 PythonRequest::ImplementationVersion(
3928 ImplementationName::PyPy,
3929 VersionRequest::from_str("3.10").unwrap(),
3930 )
3931 );
3932 assert_eq!(
3933 PythonRequest::parse("graalpy3.10"),
3934 PythonRequest::ImplementationVersion(
3935 ImplementationName::GraalPy,
3936 VersionRequest::from_str("3.10").unwrap(),
3937 )
3938 );
3939 assert_eq!(
3940 PythonRequest::parse("gp310"),
3941 PythonRequest::ImplementationVersion(
3942 ImplementationName::GraalPy,
3943 VersionRequest::from_str("3.10").unwrap(),
3944 )
3945 );
3946 assert_eq!(
3947 PythonRequest::parse("cp38"),
3948 PythonRequest::ImplementationVersion(
3949 ImplementationName::CPython,
3950 VersionRequest::from_str("3.8").unwrap(),
3951 )
3952 );
3953 assert_eq!(
3954 PythonRequest::parse("pypy@3.10"),
3955 PythonRequest::ImplementationVersion(
3956 ImplementationName::PyPy,
3957 VersionRequest::from_str("3.10").unwrap(),
3958 )
3959 );
3960 assert_eq!(
3961 PythonRequest::parse("pypy310"),
3962 PythonRequest::ImplementationVersion(
3963 ImplementationName::PyPy,
3964 VersionRequest::from_str("3.10").unwrap(),
3965 )
3966 );
3967 assert_eq!(
3968 PythonRequest::parse("graalpy@3.10"),
3969 PythonRequest::ImplementationVersion(
3970 ImplementationName::GraalPy,
3971 VersionRequest::from_str("3.10").unwrap(),
3972 )
3973 );
3974 assert_eq!(
3975 PythonRequest::parse("graalpy310"),
3976 PythonRequest::ImplementationVersion(
3977 ImplementationName::GraalPy,
3978 VersionRequest::from_str("3.10").unwrap(),
3979 )
3980 );
3981
3982 let tempdir = TempDir::new().unwrap();
3983 assert_eq!(
3984 PythonRequest::parse(tempdir.path().to_str().unwrap()),
3985 PythonRequest::Directory(tempdir.path().to_path_buf()),
3986 "An existing directory is treated as a directory"
3987 );
3988 assert_eq!(
3989 PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3990 PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3991 "A path that does not exist is treated as a file"
3992 );
3993 tempdir.child("bar").touch().unwrap();
3994 assert_eq!(
3995 PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3996 PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3997 "An existing file is treated as a file"
3998 );
3999 assert_eq!(
4000 PythonRequest::parse("./foo"),
4001 PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
4002 "A string with a file system separator is treated as a file"
4003 );
4004 assert_eq!(
4005 PythonRequest::parse("3.13t"),
4006 PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
4007 );
4008 }
4009
4010 #[test]
4011 fn discovery_sources_prefer_system_orders_search_path_first() {
4012 let preferences = DiscoveryPreferences {
4013 python_preference: PythonPreference::System,
4014 environment_preference: EnvironmentPreference::OnlySystem,
4015 };
4016 let sources = preferences.sources(&PythonRequest::Default);
4017
4018 if cfg!(windows) {
4019 assert_eq!(sources, "search path, registry, or managed installations");
4020 } else {
4021 assert_eq!(sources, "search path or managed installations");
4022 }
4023 }
4024
4025 #[test]
4026 fn discovery_sources_only_system_matches_platform_order() {
4027 let preferences = DiscoveryPreferences {
4028 python_preference: PythonPreference::OnlySystem,
4029 environment_preference: EnvironmentPreference::OnlySystem,
4030 };
4031 let sources = preferences.sources(&PythonRequest::Default);
4032
4033 if cfg!(windows) {
4034 assert_eq!(sources, "search path or registry");
4035 } else {
4036 assert_eq!(sources, "search path");
4037 }
4038 }
4039
4040 #[test]
4041 fn interpreter_request_to_canonical_string() {
4042 assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
4043 assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
4044 assert_eq!(
4045 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
4046 "3.12"
4047 );
4048 assert_eq!(
4049 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
4050 .to_canonical_string(),
4051 ">=3.12"
4052 );
4053 assert_eq!(
4054 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
4055 .to_canonical_string(),
4056 ">=3.12, <3.13"
4057 );
4058
4059 assert_eq!(
4060 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
4061 .to_canonical_string(),
4062 "3.13a1"
4063 );
4064
4065 assert_eq!(
4066 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
4067 .to_canonical_string(),
4068 "3.13b5"
4069 );
4070
4071 assert_eq!(
4072 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
4073 .to_canonical_string(),
4074 "3.13rc1"
4075 );
4076
4077 assert_eq!(
4078 PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
4079 .to_canonical_string(),
4080 "3.13rc4"
4081 );
4082
4083 assert_eq!(
4084 PythonRequest::Version(VersionRequest::from_str("3.14.5rc1").unwrap())
4085 .to_canonical_string(),
4086 "3.14.5rc1"
4087 );
4088
4089 assert_eq!(
4090 PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
4091 "foo"
4092 );
4093 assert_eq!(
4094 PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
4095 "cpython"
4096 );
4097 assert_eq!(
4098 PythonRequest::ImplementationVersion(
4099 ImplementationName::CPython,
4100 VersionRequest::from_str("3.12.2").unwrap(),
4101 )
4102 .to_canonical_string(),
4103 "cpython@3.12.2"
4104 );
4105 assert_eq!(
4106 PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
4107 "pypy"
4108 );
4109 assert_eq!(
4110 PythonRequest::ImplementationVersion(
4111 ImplementationName::PyPy,
4112 VersionRequest::from_str("3.10").unwrap(),
4113 )
4114 .to_canonical_string(),
4115 "pypy@3.10"
4116 );
4117 assert_eq!(
4118 PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
4119 "graalpy"
4120 );
4121 assert_eq!(
4122 PythonRequest::ImplementationVersion(
4123 ImplementationName::GraalPy,
4124 VersionRequest::from_str("3.10").unwrap(),
4125 )
4126 .to_canonical_string(),
4127 "graalpy@3.10"
4128 );
4129
4130 let tempdir = TempDir::new().unwrap();
4131 assert_eq!(
4132 PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
4133 tempdir.path().to_str().unwrap(),
4134 "An existing directory is treated as a directory"
4135 );
4136 assert_eq!(
4137 PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
4138 tempdir.child("foo").path().to_str().unwrap(),
4139 "A path that does not exist is treated as a file"
4140 );
4141 tempdir.child("bar").touch().unwrap();
4142 assert_eq!(
4143 PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
4144 tempdir.child("bar").path().to_str().unwrap(),
4145 "An existing file is treated as a file"
4146 );
4147 assert_eq!(
4148 PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
4149 "./foo",
4150 "A string with a file system separator is treated as a file"
4151 );
4152 }
4153
4154 #[test]
4155 fn version_request_from_str() {
4156 assert_eq!(
4157 VersionRequest::from_str("3").unwrap(),
4158 VersionRequest::Major(3, PythonVariant::Default)
4159 );
4160 assert_eq!(
4161 VersionRequest::from_str("3.12").unwrap(),
4162 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4163 );
4164 assert_eq!(
4165 VersionRequest::from_str("3.12.1").unwrap(),
4166 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4167 );
4168 assert!(VersionRequest::from_str("1.foo.1").is_err());
4169 assert_eq!(
4170 VersionRequest::from_str("3").unwrap(),
4171 VersionRequest::Major(3, PythonVariant::Default)
4172 );
4173 assert_eq!(
4174 VersionRequest::from_str("38").unwrap(),
4175 VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
4176 );
4177 assert_eq!(
4178 VersionRequest::from_str("312").unwrap(),
4179 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4180 );
4181 assert_eq!(
4182 VersionRequest::from_str("3100").unwrap(),
4183 VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
4184 );
4185 assert_eq!(
4186 VersionRequest::from_str("3.13a1").unwrap(),
4187 VersionRequest::MajorMinorPrerelease(
4188 3,
4189 13,
4190 Prerelease {
4191 kind: PrereleaseKind::Alpha,
4192 number: 1
4193 },
4194 PythonVariant::Default
4195 )
4196 );
4197 assert_eq!(
4198 VersionRequest::from_str("313b1").unwrap(),
4199 VersionRequest::MajorMinorPrerelease(
4200 3,
4201 13,
4202 Prerelease {
4203 kind: PrereleaseKind::Beta,
4204 number: 1
4205 },
4206 PythonVariant::Default
4207 )
4208 );
4209 assert_eq!(
4210 VersionRequest::from_str("3.13.0b2").unwrap(),
4211 VersionRequest::MajorMinorPrerelease(
4212 3,
4213 13,
4214 Prerelease {
4215 kind: PrereleaseKind::Beta,
4216 number: 2
4217 },
4218 PythonVariant::Default
4219 )
4220 );
4221 assert_eq!(
4222 VersionRequest::from_str("3.13.0rc3").unwrap(),
4223 VersionRequest::MajorMinorPrerelease(
4224 3,
4225 13,
4226 Prerelease {
4227 kind: PrereleaseKind::Rc,
4228 number: 3
4229 },
4230 PythonVariant::Default
4231 )
4232 );
4233 assert!(
4234 matches!(
4235 VersionRequest::from_str("3rc1"),
4236 Err(Error::InvalidVersionRequest(_))
4237 ),
4238 "Pre-release version requests require a minor version"
4239 );
4240 assert_eq!(
4241 VersionRequest::from_str("3.14.5rc1").unwrap(),
4242 VersionRequest::MajorMinorPatchPrerelease(
4243 3,
4244 14,
4245 5,
4246 Prerelease {
4247 kind: PrereleaseKind::Rc,
4248 number: 1
4249 },
4250 PythonVariant::Default
4251 ),
4252 "Pre-release version requests with a non-zero patch are allowed (e.g., `3.14.5rc1`)"
4253 );
4254 assert_eq!(
4255 VersionRequest::from_str("3.13.2rc1").unwrap(),
4256 VersionRequest::MajorMinorPatchPrerelease(
4257 3,
4258 13,
4259 2,
4260 Prerelease {
4261 kind: PrereleaseKind::Rc,
4262 number: 1
4263 },
4264 PythonVariant::Default
4265 )
4266 );
4267 assert!(
4268 matches!(
4269 VersionRequest::from_str("3.12-dev"),
4270 Err(Error::InvalidVersionRequest(_))
4271 ),
4272 "Development version segments are not allowed"
4273 );
4274 assert!(
4275 matches!(
4276 VersionRequest::from_str("3.12+local"),
4277 Err(Error::InvalidVersionRequest(_))
4278 ),
4279 "Local version segments are not allowed"
4280 );
4281 assert!(
4282 matches!(
4283 VersionRequest::from_str("3.12.post0"),
4284 Err(Error::InvalidVersionRequest(_))
4285 ),
4286 "Post version segments are not allowed"
4287 );
4288 assert!(
4289 matches!(
4291 VersionRequest::from_str("31000"),
4292 Err(Error::InvalidVersionRequest(_))
4293 )
4294 );
4295 assert_eq!(
4296 VersionRequest::from_str("3t").unwrap(),
4297 VersionRequest::Major(3, PythonVariant::Freethreaded)
4298 );
4299 assert_eq!(
4300 VersionRequest::from_str("313t").unwrap(),
4301 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4302 );
4303 assert_eq!(
4304 VersionRequest::from_str("3.13t").unwrap(),
4305 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4306 );
4307 assert_eq!(
4308 VersionRequest::from_str(">=3.13t").unwrap(),
4309 VersionRequest::Range(
4310 VersionSpecifiers::from_str(">=3.13").unwrap(),
4311 PythonVariant::Freethreaded
4312 )
4313 );
4314 assert_eq!(
4315 VersionRequest::from_str(">=3.13").unwrap(),
4316 VersionRequest::Range(
4317 VersionSpecifiers::from_str(">=3.13").unwrap(),
4318 PythonVariant::Default
4319 )
4320 );
4321 assert_eq!(
4322 VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4323 VersionRequest::Range(
4324 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4325 PythonVariant::Freethreaded
4326 )
4327 );
4328 assert!(matches!(
4329 VersionRequest::from_str("3.13tt"),
4330 Err(Error::InvalidVersionRequest(_))
4331 ));
4332 assert!(matches!(
4333 VersionRequest::from_str("3.12²t"),
4334 Err(Error::InvalidVersionRequest(_))
4335 ));
4336
4337 assert_eq!(
4339 VersionRequest::from_str("==3.12").unwrap(),
4340 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4341 );
4342 assert_eq!(
4343 VersionRequest::from_str("==3.12.1").unwrap(),
4344 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4345 );
4346 }
4347
4348 #[test]
4349 fn version_request_from_specifiers() {
4350 assert_eq!(
4352 VersionRequest::from_specifiers(
4353 VersionSpecifiers::from_str("==3.12").unwrap(),
4354 PythonVariant::Default
4355 ),
4356 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4357 );
4358 assert_eq!(
4359 VersionRequest::from_specifiers(
4360 VersionSpecifiers::from_str("==3.12.1").unwrap(),
4361 PythonVariant::Default
4362 ),
4363 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4364 );
4365
4366 assert_eq!(
4368 VersionRequest::from_specifiers(
4369 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4370 PythonVariant::Default
4371 ),
4372 VersionRequest::Range(
4373 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4374 PythonVariant::Default
4375 )
4376 );
4377
4378 assert_eq!(
4380 VersionRequest::from_specifiers(
4381 VersionSpecifiers::from_str(">=3.12").unwrap(),
4382 PythonVariant::Default
4383 ),
4384 VersionRequest::Range(
4385 VersionSpecifiers::from_str(">=3.12").unwrap(),
4386 PythonVariant::Default
4387 )
4388 );
4389
4390 assert_eq!(
4392 VersionRequest::from_specifiers(
4393 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4394 PythonVariant::Default
4395 ),
4396 VersionRequest::Range(
4397 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4398 PythonVariant::Default
4399 )
4400 );
4401 }
4402
4403 #[test]
4404 fn executable_names_from_request() {
4405 fn case(request: &str, expected: &[&str]) {
4406 let (implementation, version) = match PythonRequest::parse(request) {
4407 PythonRequest::Any => (None, VersionRequest::Any),
4408 PythonRequest::Default => (None, VersionRequest::Default),
4409 PythonRequest::Version(version) => (None, version),
4410 PythonRequest::ImplementationVersion(implementation, version) => {
4411 (Some(implementation), version)
4412 }
4413 PythonRequest::Implementation(implementation) => {
4414 (Some(implementation), VersionRequest::Default)
4415 }
4416 result => {
4417 panic!("Test cases should request versions or implementations; got {result:?}")
4418 }
4419 };
4420
4421 let result: Vec<_> = version
4422 .executable_names(implementation.as_ref())
4423 .into_iter()
4424 .map(|name| name.to_string())
4425 .collect();
4426
4427 let expected: Vec<_> = expected
4428 .iter()
4429 .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4430 .collect();
4431
4432 assert_eq!(result, expected, "mismatch for case \"{request}\"");
4433 }
4434
4435 case(
4436 "any",
4437 &[
4438 "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4439 "pyodide", "pyodide3",
4440 ],
4441 );
4442
4443 case("default", &["python", "python3"]);
4444
4445 case("3", &["python3", "python"]);
4446
4447 case("4", &["python4", "python"]);
4448
4449 case("3.13", &["python3.13", "python3", "python"]);
4450
4451 case("pypy", &["pypy", "pypy3", "python", "python3"]);
4452
4453 case(
4454 "pypy@3.10",
4455 &[
4456 "pypy3.10",
4457 "pypy3",
4458 "pypy",
4459 "python3.10",
4460 "python3",
4461 "python",
4462 ],
4463 );
4464
4465 case(
4466 "3.13t",
4467 &[
4468 "python3.13t",
4469 "python3.13",
4470 "python3t",
4471 "python3",
4472 "pythont",
4473 "python",
4474 ],
4475 );
4476 case("3t", &["python3t", "python3", "pythont", "python"]);
4477
4478 case(
4479 "3.13.2",
4480 &["python3.13.2", "python3.13", "python3", "python"],
4481 );
4482
4483 case(
4484 "3.13rc2",
4485 &["python3.13rc2", "python3.13", "python3", "python"],
4486 );
4487 }
4488
4489 #[test]
4490 fn test_try_split_prefix_and_version() {
4491 assert!(matches!(
4492 PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4493 Ok(None),
4494 ));
4495 assert!(matches!(
4496 PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4497 Ok(Some(_)),
4498 ));
4499 assert!(matches!(
4500 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4501 Ok(Some(_)),
4502 ));
4503 assert!(matches!(
4504 PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4505 Ok(None),
4506 ));
4507 assert!(
4509 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4510 );
4511 assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4513 }
4514
4515 #[test]
4516 fn version_request_as_pep440_version() {
4517 assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4519 assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4520 assert_eq!(
4521 VersionRequest::from_str(">=3.10")
4522 .unwrap()
4523 .as_pep440_version(),
4524 None
4525 );
4526
4527 assert_eq!(
4529 VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4530 Some(Version::from_str("3").unwrap())
4531 );
4532
4533 assert_eq!(
4535 VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4536 Some(Version::from_str("3.12").unwrap())
4537 );
4538
4539 assert_eq!(
4541 VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4542 Some(Version::from_str("3.12.5").unwrap())
4543 );
4544
4545 assert_eq!(
4547 VersionRequest::MajorMinorPrerelease(
4548 3,
4549 14,
4550 Prerelease {
4551 kind: PrereleaseKind::Alpha,
4552 number: 1
4553 },
4554 PythonVariant::Default
4555 )
4556 .as_pep440_version(),
4557 Some(Version::from_str("3.14.0a1").unwrap())
4558 );
4559 assert_eq!(
4560 VersionRequest::MajorMinorPrerelease(
4561 3,
4562 14,
4563 Prerelease {
4564 kind: PrereleaseKind::Beta,
4565 number: 2
4566 },
4567 PythonVariant::Default
4568 )
4569 .as_pep440_version(),
4570 Some(Version::from_str("3.14.0b2").unwrap())
4571 );
4572 assert_eq!(
4573 VersionRequest::MajorMinorPrerelease(
4574 3,
4575 13,
4576 Prerelease {
4577 kind: PrereleaseKind::Rc,
4578 number: 3
4579 },
4580 PythonVariant::Default
4581 )
4582 .as_pep440_version(),
4583 Some(Version::from_str("3.13.0rc3").unwrap())
4584 );
4585
4586 assert_eq!(
4588 VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4589 Some(Version::from_str("3").unwrap())
4590 );
4591 assert_eq!(
4592 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4593 Some(Version::from_str("3.13").unwrap())
4594 );
4595 }
4596
4597 #[test]
4598 fn python_request_as_pep440_version() {
4599 assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4601 assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4602
4603 assert_eq!(
4605 PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4606 .as_pep440_version(),
4607 Some(Version::from_str("3.11").unwrap())
4608 );
4609
4610 assert_eq!(
4612 PythonRequest::ImplementationVersion(
4613 ImplementationName::CPython,
4614 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4615 )
4616 .as_pep440_version(),
4617 Some(Version::from_str("3.12.1").unwrap())
4618 );
4619
4620 assert_eq!(
4622 PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4623 None
4624 );
4625
4626 assert_eq!(
4628 PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4629 Some(Version::from_str("3.13.2").unwrap())
4630 );
4631
4632 assert_eq!(
4634 PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4635 None
4636 );
4637
4638 assert_eq!(
4640 PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4641 None
4642 );
4643 }
4644
4645 #[test]
4646 fn intersects_requires_python_exact() {
4647 let requires_python =
4648 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4649
4650 assert!(PythonRequest::parse("3.12").intersects_requires_python(&requires_python));
4651 assert!(!PythonRequest::parse("3.11").intersects_requires_python(&requires_python));
4652 }
4653
4654 #[test]
4655 fn intersects_requires_python_major() {
4656 let requires_python =
4657 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4658
4659 assert!(PythonRequest::parse("3").intersects_requires_python(&requires_python));
4661 assert!(!PythonRequest::parse("2").intersects_requires_python(&requires_python));
4663 }
4664
4665 #[test]
4666 fn intersects_requires_python_range() {
4667 let requires_python =
4668 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4669
4670 assert!(PythonRequest::parse(">=3.12,<3.13").intersects_requires_python(&requires_python));
4671 assert!(!PythonRequest::parse(">=3.10,<3.12").intersects_requires_python(&requires_python));
4672 }
4673
4674 #[test]
4675 fn intersects_requires_python_implementation_range() {
4676 let requires_python =
4677 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4678
4679 assert!(
4680 PythonRequest::parse("cpython@>=3.12,<3.13")
4681 .intersects_requires_python(&requires_python)
4682 );
4683 assert!(
4684 !PythonRequest::parse("cpython@>=3.10,<3.12")
4685 .intersects_requires_python(&requires_python)
4686 );
4687 }
4688
4689 #[test]
4690 fn intersects_requires_python_no_version() {
4691 let requires_python =
4692 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4693
4694 assert!(PythonRequest::Any.intersects_requires_python(&requires_python));
4696 assert!(PythonRequest::Default.intersects_requires_python(&requires_python));
4697 assert!(
4698 PythonRequest::Implementation(ImplementationName::CPython)
4699 .intersects_requires_python(&requires_python)
4700 );
4701 }
4702}