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