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(crate) request: PythonRequest,
219 pub(crate) python_preference: PythonPreference,
220 pub(crate) 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 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 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) -> String {
2207 match self {
2208 Self::Any => "any".to_string(),
2209 Self::Default => "default".to_string(),
2210 Self::Version(version) => version.to_string(),
2211 Self::Directory(path) => path.display().to_string(),
2212 Self::File(path) => path.display().to_string(),
2213 Self::ExecutableName(name) => name.clone(),
2214 Self::Implementation(implementation) => implementation.to_string(),
2215 Self::ImplementationVersion(implementation, version) => {
2216 format!("{implementation}@{version}")
2217 }
2218 Self::Key(request) => request.to_string(),
2219 }
2220 }
2221
2222 pub fn as_pep440_version(&self) -> Option<Version> {
2226 match self {
2227 Self::Version(v) | Self::ImplementationVersion(_, v) => v.as_pep440_version(),
2228 Self::Key(download_request) => download_request
2229 .version()
2230 .and_then(VersionRequest::as_pep440_version),
2231 Self::Default
2232 | Self::Any
2233 | Self::Directory(_)
2234 | Self::File(_)
2235 | Self::ExecutableName(_)
2236 | Self::Implementation(_) => None,
2237 }
2238 }
2239
2240 fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
2246 match self {
2247 Self::Version(version) | Self::ImplementationVersion(_, version) => {
2248 version.as_version_specifiers()
2249 }
2250 Self::Key(download_request) => download_request
2251 .version()
2252 .and_then(VersionRequest::as_version_specifiers),
2253 Self::Default
2254 | Self::Any
2255 | Self::Directory(_)
2256 | Self::File(_)
2257 | Self::ExecutableName(_)
2258 | Self::Implementation(_) => None,
2259 }
2260 }
2261
2262 pub fn intersects_requires_python(&self, requires_python: &RequiresPython) -> bool {
2268 let Some(specifiers) = self.as_version_specifiers() else {
2269 return true;
2270 };
2271
2272 let request_range = release_specifiers_to_ranges(specifiers);
2273 let requires_python_range =
2274 release_specifiers_to_ranges(requires_python.specifiers().clone());
2275 !request_range
2276 .intersection(&requires_python_range)
2277 .is_empty()
2278 }
2279}
2280
2281impl PythonSource {
2282 pub fn is_managed(self) -> bool {
2283 matches!(self, Self::Managed)
2284 }
2285
2286 fn allows_prereleases(self) -> bool {
2288 match self {
2289 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2290 Self::SearchPath
2291 | Self::SearchPathFirst
2292 | Self::CondaPrefix
2293 | Self::BaseCondaPrefix
2294 | Self::ProvidedPath
2295 | Self::ParentInterpreter
2296 | Self::ActiveEnvironment
2297 | Self::DiscoveredEnvironment => true,
2298 }
2299 }
2300
2301 fn allows_debug(self) -> bool {
2303 match self {
2304 Self::Managed | Self::Registry | Self::MicrosoftStore => false,
2305 Self::SearchPath
2306 | Self::SearchPathFirst
2307 | Self::CondaPrefix
2308 | Self::BaseCondaPrefix
2309 | Self::ProvidedPath
2310 | Self::ParentInterpreter
2311 | Self::ActiveEnvironment
2312 | Self::DiscoveredEnvironment => true,
2313 }
2314 }
2315
2316 fn allows_alternative_implementations(self) -> bool {
2318 match self {
2319 Self::Managed
2320 | Self::Registry
2321 | Self::SearchPath
2322 | Self::SearchPathFirst
2325 | Self::MicrosoftStore => false,
2326 Self::CondaPrefix
2327 | Self::BaseCondaPrefix
2328 | Self::ProvidedPath
2329 | Self::ParentInterpreter
2330 | Self::ActiveEnvironment
2331 | Self::DiscoveredEnvironment => true,
2332 }
2333 }
2334
2335 fn is_maybe_virtualenv(self) -> bool {
2347 match self {
2348 Self::ProvidedPath
2349 | Self::ActiveEnvironment
2350 | Self::DiscoveredEnvironment
2351 | Self::CondaPrefix
2352 | Self::BaseCondaPrefix
2353 | Self::ParentInterpreter
2354 | Self::SearchPathFirst => true,
2355 Self::Managed | Self::SearchPath | Self::Registry | Self::MicrosoftStore => false,
2356 }
2357 }
2358
2359 fn is_explicit(self) -> bool {
2362 match self {
2363 Self::ProvidedPath
2364 | Self::ParentInterpreter
2365 | Self::ActiveEnvironment
2366 | Self::CondaPrefix => true,
2367 Self::Managed
2368 | Self::DiscoveredEnvironment
2369 | Self::SearchPath
2370 | Self::SearchPathFirst
2371 | Self::Registry
2372 | Self::MicrosoftStore
2373 | Self::BaseCondaPrefix => false,
2374 }
2375 }
2376
2377 fn is_maybe_system(self) -> bool {
2379 match self {
2380 Self::CondaPrefix
2381 | Self::BaseCondaPrefix
2382 | Self::ParentInterpreter
2383 | Self::ProvidedPath
2384 | Self::Managed
2385 | Self::SearchPath
2386 | Self::SearchPathFirst
2387 | Self::Registry
2388 | Self::MicrosoftStore => true,
2389 Self::ActiveEnvironment | Self::DiscoveredEnvironment => false,
2390 }
2391 }
2392}
2393
2394impl PythonPreference {
2395 fn allows_source(self, source: PythonSource) -> bool {
2396 if !matches!(
2398 source,
2399 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2400 ) {
2401 return true;
2402 }
2403
2404 match self {
2405 Self::OnlyManaged => matches!(source, PythonSource::Managed),
2406 Self::Managed | Self::System => matches!(
2407 source,
2408 PythonSource::Managed | PythonSource::SearchPath | PythonSource::Registry
2409 ),
2410 Self::OnlySystem => {
2411 matches!(source, PythonSource::SearchPath | PythonSource::Registry)
2412 }
2413 }
2414 }
2415
2416 pub(crate) fn allows_managed(self) -> bool {
2417 match self {
2418 Self::OnlySystem => false,
2419 Self::Managed | Self::System | Self::OnlyManaged => true,
2420 }
2421 }
2422
2423 fn allows_interpreter(self, interpreter: &Interpreter) -> bool {
2428 match self {
2429 Self::OnlyManaged => interpreter.is_managed(),
2430 Self::OnlySystem => !interpreter.is_managed(),
2431 Self::Managed | Self::System => true,
2432 }
2433 }
2434
2435 pub fn allows_installation(self, installation: &PythonInstallation) -> bool {
2443 let source = installation.source;
2444 let interpreter = &installation.interpreter;
2445
2446 match self {
2447 Self::OnlyManaged => {
2448 if self.allows_interpreter(interpreter) {
2449 true
2450 } else if source.is_explicit() {
2451 debug!(
2452 "Allowing unmanaged Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2453 interpreter.sys_executable().display()
2454 );
2455 true
2456 } else {
2457 debug!(
2458 "Ignoring Python interpreter at `{}`: only managed interpreters allowed",
2459 interpreter.sys_executable().display()
2460 );
2461 false
2462 }
2463 }
2464 Self::Managed | Self::System => true,
2466 Self::OnlySystem => {
2467 if self.allows_interpreter(interpreter) {
2468 true
2469 } else if source.is_explicit() {
2470 debug!(
2471 "Allowing managed Python interpreter at `{}` (in conflict with the `python-preference`) since it is from source: {source}",
2472 interpreter.sys_executable().display()
2473 );
2474 true
2475 } else {
2476 debug!(
2477 "Ignoring Python interpreter at `{}`: only system interpreters allowed",
2478 interpreter.sys_executable().display()
2479 );
2480 false
2481 }
2482 }
2483 }
2484 }
2485
2486 #[must_use]
2491 pub fn with_system_flag(self, system: bool) -> Self {
2492 match self {
2493 Self::OnlyManaged => self,
2498 Self::Managed => {
2499 if system {
2500 Self::System
2501 } else {
2502 self
2503 }
2504 }
2505 Self::System => self,
2506 Self::OnlySystem => self,
2507 }
2508 }
2509}
2510
2511impl PythonDownloads {
2512 pub fn is_automatic(self) -> bool {
2513 matches!(self, Self::Automatic)
2514 }
2515}
2516
2517impl EnvironmentPreference {
2518 pub fn from_system_flag(system: bool, mutable: bool) -> Self {
2519 match (system, mutable) {
2520 (true, _) => Self::OnlySystem,
2522 (false, true) => Self::ExplicitSystem,
2524 (false, false) => Self::Any,
2526 }
2527 }
2528
2529 pub(crate) fn allows_installation(self, installation: &PythonInstallation) -> bool {
2535 interpreter_satisfies_environment_preference(
2536 installation.source,
2537 &installation.interpreter,
2538 self,
2539 )
2540 }
2541}
2542
2543#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)]
2544pub(crate) struct ExecutableName {
2545 implementation: Option<ImplementationName>,
2546 major: Option<u8>,
2547 minor: Option<u8>,
2548 patch: Option<u8>,
2549 prerelease: Option<Prerelease>,
2550 variant: PythonVariant,
2551}
2552
2553#[derive(Debug, Clone, PartialEq, Eq)]
2554struct ExecutableNameComparator<'a> {
2555 name: ExecutableName,
2556 request: &'a VersionRequest,
2557 implementation: Option<&'a ImplementationName>,
2558}
2559
2560impl Ord for ExecutableNameComparator<'_> {
2561 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
2565 let name_ordering = if self.implementation.is_some() {
2568 std::cmp::Ordering::Greater
2569 } else {
2570 std::cmp::Ordering::Less
2571 };
2572 if self.name.implementation.is_none() && other.name.implementation.is_some() {
2573 return name_ordering.reverse();
2574 }
2575 if self.name.implementation.is_some() && other.name.implementation.is_none() {
2576 return name_ordering;
2577 }
2578 let ordering = self.name.implementation.cmp(&other.name.implementation);
2580 if ordering != std::cmp::Ordering::Equal {
2581 return ordering;
2582 }
2583 let ordering = self.name.major.cmp(&other.name.major);
2584 let is_default_request =
2585 matches!(self.request, VersionRequest::Any | VersionRequest::Default);
2586 if ordering != std::cmp::Ordering::Equal {
2587 return if is_default_request {
2588 ordering.reverse()
2589 } else {
2590 ordering
2591 };
2592 }
2593 let ordering = self.name.minor.cmp(&other.name.minor);
2594 if ordering != std::cmp::Ordering::Equal {
2595 return if is_default_request {
2596 ordering.reverse()
2597 } else {
2598 ordering
2599 };
2600 }
2601 let ordering = self.name.patch.cmp(&other.name.patch);
2602 if ordering != std::cmp::Ordering::Equal {
2603 return if is_default_request {
2604 ordering.reverse()
2605 } else {
2606 ordering
2607 };
2608 }
2609 let ordering = self.name.prerelease.cmp(&other.name.prerelease);
2610 if ordering != std::cmp::Ordering::Equal {
2611 return if is_default_request {
2612 ordering.reverse()
2613 } else {
2614 ordering
2615 };
2616 }
2617 let ordering = self.name.variant.cmp(&other.name.variant);
2618 if ordering != std::cmp::Ordering::Equal {
2619 return if is_default_request {
2620 ordering.reverse()
2621 } else {
2622 ordering
2623 };
2624 }
2625 ordering
2626 }
2627}
2628
2629impl PartialOrd for ExecutableNameComparator<'_> {
2630 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
2631 Some(self.cmp(other))
2632 }
2633}
2634
2635impl ExecutableName {
2636 #[must_use]
2637 fn with_implementation(mut self, implementation: ImplementationName) -> Self {
2638 self.implementation = Some(implementation);
2639 self
2640 }
2641
2642 #[must_use]
2643 fn with_major(mut self, major: u8) -> Self {
2644 self.major = Some(major);
2645 self
2646 }
2647
2648 #[must_use]
2649 fn with_minor(mut self, minor: u8) -> Self {
2650 self.minor = Some(minor);
2651 self
2652 }
2653
2654 #[must_use]
2655 fn with_patch(mut self, patch: u8) -> Self {
2656 self.patch = Some(patch);
2657 self
2658 }
2659
2660 #[must_use]
2661 fn with_prerelease(mut self, prerelease: Prerelease) -> Self {
2662 self.prerelease = Some(prerelease);
2663 self
2664 }
2665
2666 #[must_use]
2667 fn with_variant(mut self, variant: PythonVariant) -> Self {
2668 self.variant = variant;
2669 self
2670 }
2671
2672 fn into_comparator<'a>(
2673 self,
2674 request: &'a VersionRequest,
2675 implementation: Option<&'a ImplementationName>,
2676 ) -> ExecutableNameComparator<'a> {
2677 ExecutableNameComparator {
2678 name: self,
2679 request,
2680 implementation,
2681 }
2682 }
2683}
2684
2685impl fmt::Display for ExecutableName {
2686 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2687 if let Some(implementation) = self.implementation {
2688 write!(f, "{implementation}")?;
2689 } else {
2690 f.write_str("python")?;
2691 }
2692 if let Some(major) = self.major {
2693 write!(f, "{major}")?;
2694 if let Some(minor) = self.minor {
2695 write!(f, ".{minor}")?;
2696 if let Some(patch) = self.patch {
2697 write!(f, ".{patch}")?;
2698 }
2699 }
2700 }
2701 if let Some(prerelease) = &self.prerelease {
2702 write!(f, "{prerelease}")?;
2703 }
2704 f.write_str(self.variant.executable_suffix())?;
2705 f.write_str(EXE_SUFFIX)?;
2706 Ok(())
2707 }
2708}
2709
2710impl VersionRequest {
2711 pub fn from_specifiers(specifiers: VersionSpecifiers, variant: PythonVariant) -> Self {
2716 if let [specifier] = specifiers.iter().as_slice()
2717 && specifier.operator() == &uv_pep440::Operator::Equal
2718 && let Ok(request) = Self::from_str(&specifier.version().to_string())
2719 {
2720 return request;
2721 }
2722 Self::Range(specifiers, variant)
2723 }
2724
2725 #[must_use]
2727 pub fn only_minor(self) -> Self {
2728 match self {
2729 Self::Any => self,
2730 Self::Default => self,
2731 Self::Range(specifiers, variant) => Self::Range(
2732 specifiers
2733 .into_iter()
2734 .map(|s| s.only_minor_release())
2735 .collect(),
2736 variant,
2737 ),
2738 Self::Major(..) => self,
2739 Self::MajorMinor(..) => self,
2740 Self::MajorMinorPatch(major, minor, _, variant)
2741 | Self::MajorMinorPrerelease(major, minor, _, variant)
2742 | Self::MajorMinorPatchPrerelease(major, minor, _, _, variant) => {
2743 Self::MajorMinor(major, minor, variant)
2744 }
2745 }
2746 }
2747
2748 pub(crate) fn executable_names(
2750 &self,
2751 implementation: Option<&ImplementationName>,
2752 ) -> Vec<ExecutableName> {
2753 let prerelease = match self {
2754 Self::MajorMinorPrerelease(_, _, prerelease, _)
2755 | Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => {
2756 Some(prerelease)
2758 }
2759 _ => None,
2760 };
2761
2762 let mut names = Vec::new();
2764 names.push(ExecutableName::default());
2765
2766 if let Some(major) = self.major() {
2768 names.push(ExecutableName::default().with_major(major));
2770 if let Some(minor) = self.minor() {
2771 names.push(
2773 ExecutableName::default()
2774 .with_major(major)
2775 .with_minor(minor),
2776 );
2777 if let Some(patch) = self.patch() {
2778 names.push(
2780 ExecutableName::default()
2781 .with_major(major)
2782 .with_minor(minor)
2783 .with_patch(patch),
2784 );
2785 }
2786 }
2787 } else {
2788 names.push(ExecutableName::default().with_major(3));
2790 }
2791
2792 if let Some(prerelease) = prerelease {
2793 for i in 0..names.len() {
2795 let name = names[i];
2796 if name.minor.is_none() {
2797 continue;
2800 }
2801 names.push(name.with_prerelease(*prerelease));
2802 }
2803 }
2804
2805 if let Some(implementation) = implementation {
2807 for i in 0..names.len() {
2808 let name = names[i].with_implementation(*implementation);
2809 names.push(name);
2810 }
2811 } else {
2812 if matches!(self, Self::Any) {
2814 for i in 0..names.len() {
2815 for implementation in ImplementationName::iter_all() {
2816 let name = names[i].with_implementation(implementation);
2817 names.push(name);
2818 }
2819 }
2820 }
2821 }
2822
2823 if let Some(variant) = self.variant()
2825 && variant != PythonVariant::Default
2826 {
2827 for i in 0..names.len() {
2828 let name = names[i].with_variant(variant);
2829 names.push(name);
2830 }
2831 }
2832
2833 names.sort_unstable_by_key(|name| name.into_comparator(self, implementation));
2834 names.reverse();
2835
2836 names
2837 }
2838
2839 fn major(&self) -> Option<u8> {
2841 match self {
2842 Self::Any | Self::Default | Self::Range(_, _) => None,
2843 Self::Major(major, _) => Some(*major),
2844 Self::MajorMinor(major, _, _) => Some(*major),
2845 Self::MajorMinorPatch(major, _, _, _) => Some(*major),
2846 Self::MajorMinorPrerelease(major, _, _, _) => Some(*major),
2847 Self::MajorMinorPatchPrerelease(major, _, _, _, _) => Some(*major),
2848 }
2849 }
2850
2851 fn minor(&self) -> Option<u8> {
2853 match self {
2854 Self::Any | Self::Default | Self::Range(_, _) => None,
2855 Self::Major(_, _) => None,
2856 Self::MajorMinor(_, minor, _) => Some(*minor),
2857 Self::MajorMinorPatch(_, minor, _, _) => Some(*minor),
2858 Self::MajorMinorPrerelease(_, minor, _, _) => Some(*minor),
2859 Self::MajorMinorPatchPrerelease(_, minor, _, _, _) => Some(*minor),
2860 }
2861 }
2862
2863 fn patch(&self) -> Option<u8> {
2865 match self {
2866 Self::Any | Self::Default | Self::Range(_, _) => None,
2867 Self::Major(_, _) => None,
2868 Self::MajorMinor(_, _, _) => None,
2869 Self::MajorMinorPatch(_, _, patch, _) => Some(*patch),
2870 Self::MajorMinorPrerelease(_, _, _, _) => None,
2871 Self::MajorMinorPatchPrerelease(_, _, patch, _, _) => Some(*patch),
2872 }
2873 }
2874
2875 fn prerelease(&self) -> Option<&Prerelease> {
2877 match self {
2878 Self::Any | Self::Default | Self::Range(_, _) => None,
2879 Self::Major(_, _) => None,
2880 Self::MajorMinor(_, _, _) => None,
2881 Self::MajorMinorPatch(_, _, _, _) => None,
2882 Self::MajorMinorPrerelease(_, _, prerelease, _) => Some(prerelease),
2883 Self::MajorMinorPatchPrerelease(_, _, _, prerelease, _) => Some(prerelease),
2884 }
2885 }
2886
2887 fn check_supported(&self) -> Result<(), String> {
2891 match self {
2892 Self::Any | Self::Default => (),
2893 Self::Major(major, _) => {
2894 if *major < 3 {
2895 return Err(format!(
2896 "Python <3 is not supported but {major} was requested."
2897 ));
2898 }
2899 }
2900 Self::MajorMinor(major, minor, _) => {
2901 if (*major, *minor) < (3, 6) {
2902 return Err(format!(
2903 "Python <3.6 is not supported but {major}.{minor} was requested."
2904 ));
2905 }
2906 }
2907 Self::MajorMinorPatch(major, minor, patch, _) => {
2908 if (*major, *minor) < (3, 6) {
2909 return Err(format!(
2910 "Python <3.6 is not supported but {major}.{minor}.{patch} was requested."
2911 ));
2912 }
2913 }
2914 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
2915 if (*major, *minor) < (3, 6) {
2916 return Err(format!(
2917 "Python <3.6 is not supported but {major}.{minor}{prerelease} was requested."
2918 ));
2919 }
2920 }
2921 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
2922 if (*major, *minor) < (3, 6) {
2923 return Err(format!(
2924 "Python <3.6 is not supported but {major}.{minor}.{patch}{prerelease} was requested."
2925 ));
2926 }
2927 }
2928 Self::Range(_, _) => (),
2930 }
2931
2932 if self.is_freethreaded()
2933 && let Self::MajorMinor(major, minor, _) = self.clone().without_patch()
2934 && (major, minor) < (3, 13)
2935 {
2936 return Err(format!(
2937 "Python <3.13 does not support free-threading but {self} was requested."
2938 ));
2939 }
2940
2941 Ok(())
2942 }
2943
2944 #[must_use]
2950 fn into_request_for_source(self, source: PythonSource) -> Self {
2951 match self {
2952 Self::Default => match source {
2953 PythonSource::ParentInterpreter
2954 | PythonSource::CondaPrefix
2955 | PythonSource::BaseCondaPrefix
2956 | PythonSource::ProvidedPath
2957 | PythonSource::DiscoveredEnvironment
2958 | PythonSource::ActiveEnvironment => Self::Any,
2959 PythonSource::SearchPath
2960 | PythonSource::SearchPathFirst
2961 | PythonSource::Registry
2962 | PythonSource::MicrosoftStore
2963 | PythonSource::Managed => Self::Default,
2964 },
2965 _ => self,
2966 }
2967 }
2968
2969 pub(crate) fn matches_installation(&self, installation: &PythonInstallation) -> bool {
2972 let request = self.clone().into_request_for_source(installation.source);
2973 request.matches_interpreter(&installation.interpreter)
2974 }
2975
2976 pub(crate) fn matches_interpreter(&self, interpreter: &Interpreter) -> bool {
2978 match self {
2979 Self::Any => true,
2980 Self::Default => PythonVariant::Default.matches_interpreter(interpreter),
2982 Self::Major(major, variant) => {
2983 interpreter.python_major() == *major && variant.matches_interpreter(interpreter)
2984 }
2985 Self::MajorMinor(major, minor, variant) => {
2986 (interpreter.python_major(), interpreter.python_minor()) == (*major, *minor)
2987 && variant.matches_interpreter(interpreter)
2988 }
2989 Self::MajorMinorPatch(major, minor, patch, variant) => {
2990 (
2991 interpreter.python_major(),
2992 interpreter.python_minor(),
2993 interpreter.python_patch(),
2994 ) == (*major, *minor, *patch)
2995 && interpreter.python_version().pre().is_none()
2998 && variant.matches_interpreter(interpreter)
2999 }
3000 Self::Range(specifiers, variant) => {
3001 let version = if specifiers
3004 .iter()
3005 .any(uv_pep440::VersionSpecifier::any_prerelease)
3006 {
3007 Cow::Borrowed(interpreter.python_version())
3008 } else {
3009 Cow::Owned(interpreter.python_version().only_release())
3010 };
3011 specifiers.contains(&version) && variant.matches_interpreter(interpreter)
3012 }
3013 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3014 let version = interpreter.python_version();
3015 let Some(interpreter_prerelease) = version.pre() else {
3016 return false;
3017 };
3018 (
3019 interpreter.python_major(),
3020 interpreter.python_minor(),
3021 interpreter_prerelease,
3022 ) == (*major, *minor, *prerelease)
3023 && variant.matches_interpreter(interpreter)
3024 }
3025 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3026 let version = interpreter.python_version();
3027 let Some(interpreter_prerelease) = version.pre() else {
3028 return false;
3029 };
3030 (
3031 interpreter.python_major(),
3032 interpreter.python_minor(),
3033 interpreter.python_patch(),
3034 interpreter_prerelease,
3035 ) == (*major, *minor, *patch, *prerelease)
3036 && variant.matches_interpreter(interpreter)
3037 }
3038 }
3039 }
3040
3041 fn matches_version(&self, version: &PythonVersion) -> bool {
3046 match self {
3047 Self::Any | Self::Default => true,
3048 Self::Major(major, _) => version.major() == *major,
3049 Self::MajorMinor(major, minor, _) => {
3050 (version.major(), version.minor()) == (*major, *minor)
3051 }
3052 Self::MajorMinorPatch(major, minor, patch, _) => {
3053 (version.major(), version.minor(), version.patch())
3054 == (*major, *minor, Some(*patch))
3055 }
3056 Self::Range(specifiers, _) => {
3057 let version = if specifiers
3060 .iter()
3061 .any(uv_pep440::VersionSpecifier::any_prerelease)
3062 {
3063 Cow::Borrowed(&version.version)
3064 } else {
3065 Cow::Owned(version.version.only_release())
3066 };
3067 specifiers.contains(&version)
3068 }
3069 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3070 (version.major(), version.minor(), version.pre())
3071 == (*major, *minor, Some(*prerelease))
3072 }
3073 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3074 (
3075 version.major(),
3076 version.minor(),
3077 version.patch(),
3078 version.pre(),
3079 ) == (*major, *minor, Some(*patch), Some(*prerelease))
3080 }
3081 }
3082 }
3083
3084 fn matches_major_minor(&self, major: u8, minor: u8) -> bool {
3089 match self {
3090 Self::Any | Self::Default => true,
3091 Self::Major(self_major, _) => *self_major == major,
3092 Self::MajorMinor(self_major, self_minor, _) => {
3093 (*self_major, *self_minor) == (major, minor)
3094 }
3095 Self::MajorMinorPatch(self_major, self_minor, _, _) => {
3096 (*self_major, *self_minor) == (major, minor)
3097 }
3098 Self::Range(specifiers, _) => {
3099 let range = release_specifiers_to_ranges(specifiers.clone());
3100 let Some((lower, upper)) = range.bounding_range() else {
3101 return true;
3102 };
3103 let version = Version::new([u64::from(major), u64::from(minor)]);
3104
3105 let lower = LowerBound::new(lower.cloned());
3106 if !lower.major_minor().contains(&version) {
3107 return false;
3108 }
3109
3110 let upper = UpperBound::new(upper.cloned());
3111 if !upper.major_minor().contains(&version) {
3112 return false;
3113 }
3114
3115 true
3116 }
3117 Self::MajorMinorPrerelease(self_major, self_minor, _, _) => {
3118 (*self_major, *self_minor) == (major, minor)
3119 }
3120 Self::MajorMinorPatchPrerelease(self_major, self_minor, _, _, _) => {
3121 (*self_major, *self_minor) == (major, minor)
3122 }
3123 }
3124 }
3125
3126 pub(crate) fn matches_major_minor_patch_prerelease(
3132 &self,
3133 major: u8,
3134 minor: u8,
3135 patch: u8,
3136 prerelease: Option<Prerelease>,
3137 ) -> bool {
3138 match self {
3139 Self::Any | Self::Default => true,
3140 Self::Major(self_major, _) => *self_major == major,
3141 Self::MajorMinor(self_major, self_minor, _) => {
3142 (*self_major, *self_minor) == (major, minor)
3143 }
3144 Self::MajorMinorPatch(self_major, self_minor, self_patch, _) => {
3145 (*self_major, *self_minor, *self_patch) == (major, minor, patch)
3146 && prerelease.is_none()
3149 }
3150 Self::Range(specifiers, _) => specifiers.contains(
3151 &Version::new([u64::from(major), u64::from(minor), u64::from(patch)])
3152 .with_pre(prerelease),
3153 ),
3154 Self::MajorMinorPrerelease(self_major, self_minor, self_prerelease, _) => {
3155 (*self_major, *self_minor, 0, Some(*self_prerelease))
3157 == (major, minor, patch, prerelease)
3158 }
3159 Self::MajorMinorPatchPrerelease(
3160 self_major,
3161 self_minor,
3162 self_patch,
3163 self_prerelease,
3164 _,
3165 ) => {
3166 (
3167 *self_major,
3168 *self_minor,
3169 *self_patch,
3170 Some(*self_prerelease),
3171 ) == (major, minor, patch, prerelease)
3172 }
3173 }
3174 }
3175
3176 pub(crate) fn matches_installation_key(&self, key: &PythonInstallationKey) -> bool {
3181 self.matches_major_minor_patch_prerelease(key.major, key.minor, key.patch, key.prerelease())
3182 }
3183
3184 fn has_patch(&self) -> bool {
3186 match self {
3187 Self::Any | Self::Default => false,
3188 Self::Major(..) => false,
3189 Self::MajorMinor(..) => false,
3190 Self::MajorMinorPatch(..) => true,
3191 Self::MajorMinorPrerelease(..) => false,
3192 Self::MajorMinorPatchPrerelease(..) => true,
3193 Self::Range(_, _) => false,
3194 }
3195 }
3196
3197 #[must_use]
3201 fn without_patch(self) -> Self {
3202 match self {
3203 Self::Default => Self::Default,
3204 Self::Any => Self::Any,
3205 Self::Major(major, variant) => Self::Major(major, variant),
3206 Self::MajorMinor(major, minor, variant) => Self::MajorMinor(major, minor, variant),
3207 Self::MajorMinorPatch(major, minor, _, variant) => {
3208 Self::MajorMinor(major, minor, variant)
3209 }
3210 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3211 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3212 }
3213 Self::MajorMinorPatchPrerelease(major, minor, _, prerelease, variant) => {
3214 Self::MajorMinorPrerelease(major, minor, prerelease, variant)
3215 }
3216 Self::Range(_, _) => self,
3217 }
3218 }
3219
3220 pub(crate) fn allows_prereleases(&self) -> bool {
3222 match self {
3223 Self::Default => false,
3224 Self::Any => true,
3225 Self::Major(..) => false,
3226 Self::MajorMinor(..) => false,
3227 Self::MajorMinorPatch(..) => false,
3228 Self::MajorMinorPrerelease(..) => true,
3229 Self::MajorMinorPatchPrerelease(..) => true,
3230 Self::Range(specifiers, _) => specifiers.iter().any(VersionSpecifier::any_prerelease),
3231 }
3232 }
3233
3234 pub(crate) fn is_debug(&self) -> bool {
3236 match self {
3237 Self::Any | Self::Default => false,
3238 Self::Major(_, variant)
3239 | Self::MajorMinor(_, _, variant)
3240 | Self::MajorMinorPatch(_, _, _, variant)
3241 | Self::MajorMinorPrerelease(_, _, _, variant)
3242 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3243 | Self::Range(_, variant) => variant.is_debug(),
3244 }
3245 }
3246
3247 fn is_freethreaded(&self) -> bool {
3249 match self {
3250 Self::Any | Self::Default => false,
3251 Self::Major(_, variant)
3252 | Self::MajorMinor(_, _, variant)
3253 | Self::MajorMinorPatch(_, _, _, variant)
3254 | Self::MajorMinorPrerelease(_, _, _, variant)
3255 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3256 | Self::Range(_, variant) => variant.is_freethreaded(),
3257 }
3258 }
3259
3260 pub(crate) fn variant(&self) -> Option<PythonVariant> {
3262 match self {
3263 Self::Any => None,
3264 Self::Default => Some(PythonVariant::Default),
3265 Self::Major(_, variant)
3266 | Self::MajorMinor(_, _, variant)
3267 | Self::MajorMinorPatch(_, _, _, variant)
3268 | Self::MajorMinorPrerelease(_, _, _, variant)
3269 | Self::MajorMinorPatchPrerelease(_, _, _, _, variant)
3270 | Self::Range(_, variant) => Some(*variant),
3271 }
3272 }
3273
3274 fn as_pep440_version(&self) -> Option<Version> {
3278 match self {
3279 Self::Default | Self::Any | Self::Range(_, _) => None,
3280 Self::Major(major, _) => Some(Version::new([u64::from(*major)])),
3281 Self::MajorMinor(major, minor, _) => {
3282 Some(Version::new([u64::from(*major), u64::from(*minor)]))
3283 }
3284 Self::MajorMinorPatch(major, minor, patch, _) => Some(Version::new([
3285 u64::from(*major),
3286 u64::from(*minor),
3287 u64::from(*patch),
3288 ])),
3289 Self::MajorMinorPrerelease(major, minor, prerelease, _) => Some(
3291 Version::new([u64::from(*major), u64::from(*minor), 0]).with_pre(Some(*prerelease)),
3292 ),
3293 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => Some(
3294 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3295 .with_pre(Some(*prerelease)),
3296 ),
3297 }
3298 }
3299
3300 fn as_version_specifiers(&self) -> Option<VersionSpecifiers> {
3306 match self {
3307 Self::Default | Self::Any => None,
3308 Self::Major(major, _) => Some(VersionSpecifiers::from(
3309 VersionSpecifier::equals_star_version(Version::new([u64::from(*major)])),
3310 )),
3311 Self::MajorMinor(major, minor, _) => Some(VersionSpecifiers::from(
3312 VersionSpecifier::equals_star_version(Version::new([
3313 u64::from(*major),
3314 u64::from(*minor),
3315 ])),
3316 )),
3317 Self::MajorMinorPatch(major, minor, patch, _) => {
3318 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3319 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)]),
3320 )))
3321 }
3322 Self::MajorMinorPrerelease(major, minor, prerelease, _) => {
3323 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3324 Version::new([u64::from(*major), u64::from(*minor), 0])
3325 .with_pre(Some(*prerelease)),
3326 )))
3327 }
3328 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, _) => {
3329 Some(VersionSpecifiers::from(VersionSpecifier::equals_version(
3330 Version::new([u64::from(*major), u64::from(*minor), u64::from(*patch)])
3331 .with_pre(Some(*prerelease)),
3332 )))
3333 }
3334 Self::Range(specifiers, _) => Some(specifiers.clone()),
3335 }
3336 }
3337}
3338
3339impl FromStr for VersionRequest {
3340 type Err = Error;
3341
3342 fn from_str(s: &str) -> Result<Self, Self::Err> {
3343 fn parse_variant(s: &str) -> Result<(&str, PythonVariant), Error> {
3346 if s.chars().all(char::is_alphabetic) {
3348 return Err(Error::InvalidVersionRequest(s.to_string()));
3349 }
3350
3351 let Some(mut start) = s.rfind(|c: char| c.is_ascii_digit()) else {
3352 return Ok((s, PythonVariant::Default));
3353 };
3354
3355 start += 1;
3357
3358 if start + 1 > s.len() {
3360 return Ok((s, PythonVariant::Default));
3361 }
3362
3363 let variant = &s[start..];
3364 let prefix = &s[..start];
3365
3366 let variant = variant.strip_prefix('+').unwrap_or(variant);
3368
3369 let Ok(variant) = PythonVariant::from_str(variant) else {
3373 return Ok((s, PythonVariant::Default));
3374 };
3375
3376 Ok((prefix, variant))
3377 }
3378
3379 let (s, variant) = parse_variant(s)?;
3380 let Ok(version) = Version::from_str(s) else {
3381 return parse_version_specifiers_request(s, variant);
3382 };
3383
3384 let version = split_wheel_tag_release_version(version);
3386
3387 if version.post().is_some() || version.dev().is_some() {
3389 return Err(Error::InvalidVersionRequest(s.to_string()));
3390 }
3391
3392 if !version.local().is_empty() {
3395 return Err(Error::InvalidVersionRequest(s.to_string()));
3396 }
3397
3398 let Ok(release) = try_into_u8_slice(&version.release()) else {
3400 return Err(Error::InvalidVersionRequest(s.to_string()));
3401 };
3402
3403 let prerelease = version.pre();
3404
3405 match release.as_slice() {
3406 [major] => {
3408 if prerelease.is_some() {
3410 return Err(Error::InvalidVersionRequest(s.to_string()));
3411 }
3412 Ok(Self::Major(*major, variant))
3413 }
3414 [major, minor] => {
3416 if let Some(prerelease) = prerelease {
3417 return Ok(Self::MajorMinorPrerelease(
3418 *major, *minor, prerelease, variant,
3419 ));
3420 }
3421 Ok(Self::MajorMinor(*major, *minor, variant))
3422 }
3423 [major, minor, patch] => {
3425 if let Some(prerelease) = prerelease {
3426 if *patch == 0 {
3427 return Ok(Self::MajorMinorPrerelease(
3428 *major, *minor, prerelease, variant,
3429 ));
3430 }
3431 return Ok(Self::MajorMinorPatchPrerelease(
3432 *major, *minor, *patch, prerelease, variant,
3433 ));
3434 }
3435 Ok(Self::MajorMinorPatch(*major, *minor, *patch, variant))
3436 }
3437 _ => Err(Error::InvalidVersionRequest(s.to_string())),
3438 }
3439 }
3440}
3441
3442impl FromStr for PythonVariant {
3443 type Err = ();
3444
3445 fn from_str(s: &str) -> Result<Self, Self::Err> {
3446 match s {
3447 "t" | "freethreaded" => Ok(Self::Freethreaded),
3448 "d" | "debug" => Ok(Self::Debug),
3449 "td" | "freethreaded+debug" => Ok(Self::FreethreadedDebug),
3450 "gil" => Ok(Self::Gil),
3451 "gil+debug" => Ok(Self::GilDebug),
3452 "" => Ok(Self::Default),
3453 _ => Err(()),
3454 }
3455 }
3456}
3457
3458impl fmt::Display for PythonVariant {
3459 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3460 match self {
3461 Self::Default => f.write_str("default"),
3462 Self::Debug => f.write_str("debug"),
3463 Self::Freethreaded => f.write_str("freethreaded"),
3464 Self::FreethreadedDebug => f.write_str("freethreaded+debug"),
3465 Self::Gil => f.write_str("gil"),
3466 Self::GilDebug => f.write_str("gil+debug"),
3467 }
3468 }
3469}
3470
3471fn parse_version_specifiers_request(
3472 s: &str,
3473 variant: PythonVariant,
3474) -> Result<VersionRequest, Error> {
3475 let Ok(specifiers) = VersionSpecifiers::from_str(s) else {
3476 return Err(Error::InvalidVersionRequest(s.to_string()));
3477 };
3478 if specifiers.is_empty() {
3479 return Err(Error::InvalidVersionRequest(s.to_string()));
3480 }
3481 Ok(VersionRequest::from_specifiers(specifiers, variant))
3482}
3483
3484impl From<&PythonVersion> for VersionRequest {
3485 fn from(version: &PythonVersion) -> Self {
3486 Self::from_str(&version.string)
3487 .expect("Valid `PythonVersion`s should be valid `VersionRequest`s")
3488 }
3489}
3490
3491impl fmt::Display for VersionRequest {
3492 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3493 match self {
3494 Self::Any => f.write_str("any"),
3495 Self::Default => f.write_str("default"),
3496 Self::Major(major, variant) => write!(f, "{major}{}", variant.display_suffix()),
3497 Self::MajorMinor(major, minor, variant) => {
3498 write!(f, "{major}.{minor}{}", variant.display_suffix())
3499 }
3500 Self::MajorMinorPatch(major, minor, patch, variant) => {
3501 write!(f, "{major}.{minor}.{patch}{}", variant.display_suffix())
3502 }
3503 Self::MajorMinorPrerelease(major, minor, prerelease, variant) => {
3504 write!(f, "{major}.{minor}{prerelease}{}", variant.display_suffix())
3505 }
3506 Self::MajorMinorPatchPrerelease(major, minor, patch, prerelease, variant) => {
3507 write!(
3508 f,
3509 "{major}.{minor}.{patch}{prerelease}{}",
3510 variant.display_suffix()
3511 )
3512 }
3513 Self::Range(specifiers, _) => write!(f, "{specifiers}"),
3514 }
3515 }
3516}
3517
3518impl fmt::Display for PythonRequest {
3519 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3520 match self {
3521 Self::Default => write!(f, "a default Python"),
3522 Self::Any => write!(f, "any Python"),
3523 Self::Version(version) => write!(f, "Python {version}"),
3524 Self::Directory(path) => write!(f, "directory `{}`", path.user_display()),
3525 Self::File(path) => write!(f, "path `{}`", path.user_display()),
3526 Self::ExecutableName(name) => write!(f, "executable name `{name}`"),
3527 Self::Implementation(implementation) => {
3528 write!(f, "{}", implementation.pretty())
3529 }
3530 Self::ImplementationVersion(implementation, version) => {
3531 write!(f, "{} {version}", implementation.pretty())
3532 }
3533 Self::Key(request) => write!(f, "{request}"),
3534 }
3535 }
3536}
3537
3538impl fmt::Display for PythonSource {
3539 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3540 match self {
3541 Self::ProvidedPath => f.write_str("provided path"),
3542 Self::ActiveEnvironment => f.write_str("active virtual environment"),
3543 Self::CondaPrefix | Self::BaseCondaPrefix => f.write_str("conda prefix"),
3544 Self::DiscoveredEnvironment => f.write_str("virtual environment"),
3545 Self::SearchPath => f.write_str("search path"),
3546 Self::SearchPathFirst => f.write_str("first executable in the search path"),
3547 Self::Registry => f.write_str("registry"),
3548 Self::MicrosoftStore => f.write_str("Microsoft Store"),
3549 Self::Managed => f.write_str("managed installations"),
3550 Self::ParentInterpreter => f.write_str("parent interpreter"),
3551 }
3552 }
3553}
3554
3555impl PythonPreference {
3556 fn sources(self) -> &'static [PythonSource] {
3559 match self {
3560 Self::OnlyManaged => &[PythonSource::Managed],
3561 Self::Managed => {
3562 if cfg!(windows) {
3563 &[
3564 PythonSource::Managed,
3565 PythonSource::SearchPath,
3566 PythonSource::Registry,
3567 ]
3568 } else {
3569 &[PythonSource::Managed, PythonSource::SearchPath]
3570 }
3571 }
3572 Self::System => {
3573 if cfg!(windows) {
3574 &[
3575 PythonSource::SearchPath,
3576 PythonSource::Registry,
3577 PythonSource::Managed,
3578 ]
3579 } else {
3580 &[PythonSource::SearchPath, PythonSource::Managed]
3581 }
3582 }
3583 Self::OnlySystem => {
3584 if cfg!(windows) {
3585 &[PythonSource::SearchPath, PythonSource::Registry]
3586 } else {
3587 &[PythonSource::SearchPath]
3588 }
3589 }
3590 }
3591 }
3592}
3593
3594impl fmt::Display for PythonPreference {
3595 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
3596 f.write_str(match self {
3597 Self::OnlyManaged => "only managed",
3598 Self::Managed => "prefer managed",
3599 Self::System => "prefer system",
3600 Self::OnlySystem => "only system",
3601 })
3602 }
3603}
3604
3605impl DiscoveryPreferences {
3606 fn sources(&self, request: &PythonRequest) -> String {
3609 let python_sources = self
3610 .python_preference
3611 .sources()
3612 .iter()
3613 .map(ToString::to_string)
3614 .collect::<Vec<_>>();
3615 match self.environment_preference {
3616 EnvironmentPreference::Any => disjunction(
3617 &["virtual environments"]
3618 .into_iter()
3619 .chain(python_sources.iter().map(String::as_str))
3620 .collect::<Vec<_>>(),
3621 ),
3622 EnvironmentPreference::ExplicitSystem => {
3623 if request.is_explicit_system() {
3624 disjunction(
3625 &["virtual environments"]
3626 .into_iter()
3627 .chain(python_sources.iter().map(String::as_str))
3628 .collect::<Vec<_>>(),
3629 )
3630 } else {
3631 disjunction(&["virtual environments"])
3632 }
3633 }
3634 EnvironmentPreference::OnlySystem => disjunction(
3635 &python_sources
3636 .iter()
3637 .map(String::as_str)
3638 .collect::<Vec<_>>(),
3639 ),
3640 EnvironmentPreference::OnlyVirtual => disjunction(&["virtual environments"]),
3641 }
3642 }
3643}
3644
3645impl fmt::Display for PythonNotFound {
3646 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
3647 let sources = DiscoveryPreferences {
3648 python_preference: self.python_preference,
3649 environment_preference: self.environment_preference,
3650 }
3651 .sources(&self.request);
3652
3653 match self.request {
3654 PythonRequest::Default | PythonRequest::Any => {
3655 write!(f, "No interpreter found in {sources}")
3656 }
3657 PythonRequest::File(_) => {
3658 write!(f, "No interpreter found at {}", self.request)
3659 }
3660 PythonRequest::Directory(_) => {
3661 write!(f, "No interpreter found in {}", self.request)
3662 }
3663 _ => {
3664 write!(f, "No interpreter found for {} in {sources}", self.request)
3665 }
3666 }
3667 }
3668}
3669
3670fn disjunction(items: &[&str]) -> String {
3672 match items.len() {
3673 0 => String::new(),
3674 1 => items[0].to_string(),
3675 2 => format!("{} or {}", items[0], items[1]),
3676 _ => {
3677 let last = items.last().unwrap();
3678 format!(
3679 "{}, or {}",
3680 items.iter().take(items.len() - 1).join(", "),
3681 last
3682 )
3683 }
3684 }
3685}
3686
3687fn try_into_u8_slice(release: &[u64]) -> Result<Vec<u8>, std::num::TryFromIntError> {
3688 release
3689 .iter()
3690 .map(|x| match u8::try_from(*x) {
3691 Ok(x) => Ok(x),
3692 Err(e) => Err(e),
3693 })
3694 .collect()
3695}
3696
3697fn split_wheel_tag_release_version(version: Version) -> Version {
3704 let release = version.release();
3705 if release.len() != 1 {
3706 return version;
3707 }
3708
3709 let release = release[0].to_string();
3710 let mut chars = release.chars();
3711 let Some(major) = chars.next().and_then(|c| c.to_digit(10)) else {
3712 return version;
3713 };
3714
3715 let Ok(minor) = chars.as_str().parse::<u32>() else {
3716 return version;
3717 };
3718
3719 version.with_release([u64::from(major), u64::from(minor)])
3720}
3721
3722#[cfg(test)]
3723mod tests {
3724 use std::{cell::Cell, path::PathBuf, str::FromStr};
3725
3726 use assert_fs::{TempDir, prelude::*};
3727 use target_lexicon::{Aarch64Architecture, Architecture};
3728 use test_log::test;
3729 use uv_cache::Cache;
3730 use uv_distribution_types::RequiresPython;
3731 use uv_pep440::{Prerelease, PrereleaseKind, Version, VersionSpecifiers};
3732
3733 use crate::{
3734 discovery::{PythonRequest, VersionRequest},
3735 downloads::{ArchRequest, PythonDownloadRequest},
3736 implementation::ImplementationName,
3737 };
3738 use uv_platform::{Arch, Libc, Os};
3739
3740 use super::{
3741 DiscoveryPreferences, EnvironmentPreference, Error, PythonPreference, PythonSource,
3742 PythonVariant, QueryStrategy, python_installations_from_executables,
3743 };
3744
3745 #[test]
3746 fn sequential_query_strategy_does_not_prefetch_executables() -> anyhow::Result<()> {
3747 let cache = Cache::temp()?;
3748 let pulls = Cell::new(0);
3749 let executables = (0..2).map(|_| {
3750 pulls.set(pulls.get() + 1);
3751 Err::<(PythonSource, PathBuf), _>(Error::SourceNotAllowed(
3752 PythonRequest::Default,
3753 PythonSource::SearchPath,
3754 PythonPreference::OnlyManaged,
3755 ))
3756 });
3757
3758 let mut installations =
3759 python_installations_from_executables(executables, &cache, QueryStrategy::Sequential);
3760
3761 assert_eq!(pulls.get(), 0);
3762 assert!(installations.next().is_some_and(|result| result.is_err()));
3763 assert_eq!(pulls.get(), 1);
3764
3765 Ok(())
3766 }
3767
3768 #[test]
3769 fn interpreter_request_from_str() {
3770 assert_eq!(PythonRequest::parse("any"), PythonRequest::Any);
3771 assert_eq!(PythonRequest::parse("default"), PythonRequest::Default);
3772 assert_eq!(
3773 PythonRequest::parse("3.12"),
3774 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap())
3775 );
3776 assert_eq!(
3777 PythonRequest::parse(">=3.12"),
3778 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
3779 );
3780 assert_eq!(
3781 PythonRequest::parse(">=3.12,<3.13"),
3782 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3783 );
3784 assert_eq!(
3785 PythonRequest::parse(">=3.12,<3.13"),
3786 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
3787 );
3788
3789 assert_eq!(
3790 PythonRequest::parse("3.13.0a1"),
3791 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
3792 );
3793 assert_eq!(
3794 PythonRequest::parse("3.13.0b5"),
3795 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
3796 );
3797 assert_eq!(
3798 PythonRequest::parse("3.13.0rc1"),
3799 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
3800 );
3801 assert_eq!(
3802 PythonRequest::parse("3.13.1rc1"),
3803 PythonRequest::ExecutableName("3.13.1rc1".to_string()),
3804 "Pre-release version requests require a patch version of zero"
3805 );
3806 assert_eq!(
3807 PythonRequest::parse("3rc1"),
3808 PythonRequest::ExecutableName("3rc1".to_string()),
3809 "Pre-release version requests require a minor version"
3810 );
3811
3812 assert_eq!(
3813 PythonRequest::parse("cpython"),
3814 PythonRequest::Implementation(ImplementationName::CPython)
3815 );
3816
3817 assert_eq!(
3818 PythonRequest::parse("cpython3.12.2"),
3819 PythonRequest::ImplementationVersion(
3820 ImplementationName::CPython,
3821 VersionRequest::from_str("3.12.2").unwrap(),
3822 )
3823 );
3824
3825 assert_eq!(
3826 PythonRequest::parse("cpython-3.13.2"),
3827 PythonRequest::Key(PythonDownloadRequest {
3828 version: Some(VersionRequest::MajorMinorPatch(
3829 3,
3830 13,
3831 2,
3832 PythonVariant::Default
3833 )),
3834 implementation: Some(ImplementationName::CPython),
3835 arch: None,
3836 os: None,
3837 libc: None,
3838 build: None,
3839 prereleases: None
3840 })
3841 );
3842 assert_eq!(
3843 PythonRequest::parse("cpython-3.13.2-macos-aarch64-none"),
3844 PythonRequest::Key(PythonDownloadRequest {
3845 version: Some(VersionRequest::MajorMinorPatch(
3846 3,
3847 13,
3848 2,
3849 PythonVariant::Default
3850 )),
3851 implementation: Some(ImplementationName::CPython),
3852 arch: Some(ArchRequest::Explicit(Arch::new(
3853 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3854 None
3855 ))),
3856 os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
3857 libc: Some(Libc::None),
3858 build: None,
3859 prereleases: None
3860 })
3861 );
3862 assert_eq!(
3863 PythonRequest::parse("any-3.13.2"),
3864 PythonRequest::Key(PythonDownloadRequest {
3865 version: Some(VersionRequest::MajorMinorPatch(
3866 3,
3867 13,
3868 2,
3869 PythonVariant::Default
3870 )),
3871 implementation: None,
3872 arch: None,
3873 os: None,
3874 libc: None,
3875 build: None,
3876 prereleases: None
3877 })
3878 );
3879 assert_eq!(
3880 PythonRequest::parse("any-3.13.2-any-aarch64"),
3881 PythonRequest::Key(PythonDownloadRequest {
3882 version: Some(VersionRequest::MajorMinorPatch(
3883 3,
3884 13,
3885 2,
3886 PythonVariant::Default
3887 )),
3888 implementation: None,
3889 arch: Some(ArchRequest::Explicit(Arch::new(
3890 Architecture::Aarch64(Aarch64Architecture::Aarch64),
3891 None
3892 ))),
3893 os: None,
3894 libc: None,
3895 build: None,
3896 prereleases: None
3897 })
3898 );
3899
3900 assert_eq!(
3901 PythonRequest::parse("pypy"),
3902 PythonRequest::Implementation(ImplementationName::PyPy)
3903 );
3904 assert_eq!(
3905 PythonRequest::parse("pp"),
3906 PythonRequest::Implementation(ImplementationName::PyPy)
3907 );
3908 assert_eq!(
3909 PythonRequest::parse("graalpy"),
3910 PythonRequest::Implementation(ImplementationName::GraalPy)
3911 );
3912 assert_eq!(
3913 PythonRequest::parse("gp"),
3914 PythonRequest::Implementation(ImplementationName::GraalPy)
3915 );
3916 assert_eq!(
3917 PythonRequest::parse("cp"),
3918 PythonRequest::Implementation(ImplementationName::CPython)
3919 );
3920 assert_eq!(
3921 PythonRequest::parse("pypy3.10"),
3922 PythonRequest::ImplementationVersion(
3923 ImplementationName::PyPy,
3924 VersionRequest::from_str("3.10").unwrap(),
3925 )
3926 );
3927 assert_eq!(
3928 PythonRequest::parse("pp310"),
3929 PythonRequest::ImplementationVersion(
3930 ImplementationName::PyPy,
3931 VersionRequest::from_str("3.10").unwrap(),
3932 )
3933 );
3934 assert_eq!(
3935 PythonRequest::parse("graalpy3.10"),
3936 PythonRequest::ImplementationVersion(
3937 ImplementationName::GraalPy,
3938 VersionRequest::from_str("3.10").unwrap(),
3939 )
3940 );
3941 assert_eq!(
3942 PythonRequest::parse("gp310"),
3943 PythonRequest::ImplementationVersion(
3944 ImplementationName::GraalPy,
3945 VersionRequest::from_str("3.10").unwrap(),
3946 )
3947 );
3948 assert_eq!(
3949 PythonRequest::parse("cp38"),
3950 PythonRequest::ImplementationVersion(
3951 ImplementationName::CPython,
3952 VersionRequest::from_str("3.8").unwrap(),
3953 )
3954 );
3955 assert_eq!(
3956 PythonRequest::parse("pypy@3.10"),
3957 PythonRequest::ImplementationVersion(
3958 ImplementationName::PyPy,
3959 VersionRequest::from_str("3.10").unwrap(),
3960 )
3961 );
3962 assert_eq!(
3963 PythonRequest::parse("pypy310"),
3964 PythonRequest::ImplementationVersion(
3965 ImplementationName::PyPy,
3966 VersionRequest::from_str("3.10").unwrap(),
3967 )
3968 );
3969 assert_eq!(
3970 PythonRequest::parse("graalpy@3.10"),
3971 PythonRequest::ImplementationVersion(
3972 ImplementationName::GraalPy,
3973 VersionRequest::from_str("3.10").unwrap(),
3974 )
3975 );
3976 assert_eq!(
3977 PythonRequest::parse("graalpy310"),
3978 PythonRequest::ImplementationVersion(
3979 ImplementationName::GraalPy,
3980 VersionRequest::from_str("3.10").unwrap(),
3981 )
3982 );
3983
3984 let tempdir = TempDir::new().unwrap();
3985 assert_eq!(
3986 PythonRequest::parse(tempdir.path().to_str().unwrap()),
3987 PythonRequest::Directory(tempdir.path().to_path_buf()),
3988 "An existing directory is treated as a directory"
3989 );
3990 assert_eq!(
3991 PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()),
3992 PythonRequest::File(tempdir.child("foo").path().to_path_buf()),
3993 "A path that does not exist is treated as a file"
3994 );
3995 tempdir.child("bar").touch().unwrap();
3996 assert_eq!(
3997 PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()),
3998 PythonRequest::File(tempdir.child("bar").path().to_path_buf()),
3999 "An existing file is treated as a file"
4000 );
4001 assert_eq!(
4002 PythonRequest::parse("./foo"),
4003 PythonRequest::File(PathBuf::from_str("./foo").unwrap()),
4004 "A string with a file system separator is treated as a file"
4005 );
4006 assert_eq!(
4007 PythonRequest::parse("3.13t"),
4008 PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap())
4009 );
4010 }
4011
4012 #[test]
4013 fn discovery_sources_prefer_system_orders_search_path_first() {
4014 let preferences = DiscoveryPreferences {
4015 python_preference: PythonPreference::System,
4016 environment_preference: EnvironmentPreference::OnlySystem,
4017 };
4018 let sources = preferences.sources(&PythonRequest::Default);
4019
4020 if cfg!(windows) {
4021 assert_eq!(sources, "search path, registry, or managed installations");
4022 } else {
4023 assert_eq!(sources, "search path or managed installations");
4024 }
4025 }
4026
4027 #[test]
4028 fn discovery_sources_only_system_matches_platform_order() {
4029 let preferences = DiscoveryPreferences {
4030 python_preference: PythonPreference::OnlySystem,
4031 environment_preference: EnvironmentPreference::OnlySystem,
4032 };
4033 let sources = preferences.sources(&PythonRequest::Default);
4034
4035 if cfg!(windows) {
4036 assert_eq!(sources, "search path or registry");
4037 } else {
4038 assert_eq!(sources, "search path");
4039 }
4040 }
4041
4042 #[test]
4043 fn interpreter_request_to_canonical_string() {
4044 assert_eq!(PythonRequest::Default.to_canonical_string(), "default");
4045 assert_eq!(PythonRequest::Any.to_canonical_string(), "any");
4046 assert_eq!(
4047 PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(),
4048 "3.12"
4049 );
4050 assert_eq!(
4051 PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap())
4052 .to_canonical_string(),
4053 ">=3.12"
4054 );
4055 assert_eq!(
4056 PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap())
4057 .to_canonical_string(),
4058 ">=3.12, <3.13"
4059 );
4060
4061 assert_eq!(
4062 PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap())
4063 .to_canonical_string(),
4064 "3.13a1"
4065 );
4066
4067 assert_eq!(
4068 PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap())
4069 .to_canonical_string(),
4070 "3.13b5"
4071 );
4072
4073 assert_eq!(
4074 PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap())
4075 .to_canonical_string(),
4076 "3.13rc1"
4077 );
4078
4079 assert_eq!(
4080 PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap())
4081 .to_canonical_string(),
4082 "3.13rc4"
4083 );
4084
4085 assert_eq!(
4086 PythonRequest::Version(VersionRequest::from_str("3.14.5rc1").unwrap())
4087 .to_canonical_string(),
4088 "3.14.5rc1"
4089 );
4090
4091 assert_eq!(
4092 PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(),
4093 "foo"
4094 );
4095 assert_eq!(
4096 PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(),
4097 "cpython"
4098 );
4099 assert_eq!(
4100 PythonRequest::ImplementationVersion(
4101 ImplementationName::CPython,
4102 VersionRequest::from_str("3.12.2").unwrap(),
4103 )
4104 .to_canonical_string(),
4105 "cpython@3.12.2"
4106 );
4107 assert_eq!(
4108 PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(),
4109 "pypy"
4110 );
4111 assert_eq!(
4112 PythonRequest::ImplementationVersion(
4113 ImplementationName::PyPy,
4114 VersionRequest::from_str("3.10").unwrap(),
4115 )
4116 .to_canonical_string(),
4117 "pypy@3.10"
4118 );
4119 assert_eq!(
4120 PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(),
4121 "graalpy"
4122 );
4123 assert_eq!(
4124 PythonRequest::ImplementationVersion(
4125 ImplementationName::GraalPy,
4126 VersionRequest::from_str("3.10").unwrap(),
4127 )
4128 .to_canonical_string(),
4129 "graalpy@3.10"
4130 );
4131
4132 let tempdir = TempDir::new().unwrap();
4133 assert_eq!(
4134 PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(),
4135 tempdir.path().to_str().unwrap(),
4136 "An existing directory is treated as a directory"
4137 );
4138 assert_eq!(
4139 PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(),
4140 tempdir.child("foo").path().to_str().unwrap(),
4141 "A path that does not exist is treated as a file"
4142 );
4143 tempdir.child("bar").touch().unwrap();
4144 assert_eq!(
4145 PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(),
4146 tempdir.child("bar").path().to_str().unwrap(),
4147 "An existing file is treated as a file"
4148 );
4149 assert_eq!(
4150 PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(),
4151 "./foo",
4152 "A string with a file system separator is treated as a file"
4153 );
4154 }
4155
4156 #[test]
4157 fn version_request_from_str() {
4158 assert_eq!(
4159 VersionRequest::from_str("3").unwrap(),
4160 VersionRequest::Major(3, PythonVariant::Default)
4161 );
4162 assert_eq!(
4163 VersionRequest::from_str("3.12").unwrap(),
4164 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4165 );
4166 assert_eq!(
4167 VersionRequest::from_str("3.12.1").unwrap(),
4168 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4169 );
4170 assert!(VersionRequest::from_str("1.foo.1").is_err());
4171 assert_eq!(
4172 VersionRequest::from_str("3").unwrap(),
4173 VersionRequest::Major(3, PythonVariant::Default)
4174 );
4175 assert_eq!(
4176 VersionRequest::from_str("38").unwrap(),
4177 VersionRequest::MajorMinor(3, 8, PythonVariant::Default)
4178 );
4179 assert_eq!(
4180 VersionRequest::from_str("312").unwrap(),
4181 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4182 );
4183 assert_eq!(
4184 VersionRequest::from_str("3100").unwrap(),
4185 VersionRequest::MajorMinor(3, 100, PythonVariant::Default)
4186 );
4187 assert_eq!(
4188 VersionRequest::from_str("3.13a1").unwrap(),
4189 VersionRequest::MajorMinorPrerelease(
4190 3,
4191 13,
4192 Prerelease {
4193 kind: PrereleaseKind::Alpha,
4194 number: 1
4195 },
4196 PythonVariant::Default
4197 )
4198 );
4199 assert_eq!(
4200 VersionRequest::from_str("313b1").unwrap(),
4201 VersionRequest::MajorMinorPrerelease(
4202 3,
4203 13,
4204 Prerelease {
4205 kind: PrereleaseKind::Beta,
4206 number: 1
4207 },
4208 PythonVariant::Default
4209 )
4210 );
4211 assert_eq!(
4212 VersionRequest::from_str("3.13.0b2").unwrap(),
4213 VersionRequest::MajorMinorPrerelease(
4214 3,
4215 13,
4216 Prerelease {
4217 kind: PrereleaseKind::Beta,
4218 number: 2
4219 },
4220 PythonVariant::Default
4221 )
4222 );
4223 assert_eq!(
4224 VersionRequest::from_str("3.13.0rc3").unwrap(),
4225 VersionRequest::MajorMinorPrerelease(
4226 3,
4227 13,
4228 Prerelease {
4229 kind: PrereleaseKind::Rc,
4230 number: 3
4231 },
4232 PythonVariant::Default
4233 )
4234 );
4235 assert!(
4236 matches!(
4237 VersionRequest::from_str("3rc1"),
4238 Err(Error::InvalidVersionRequest(_))
4239 ),
4240 "Pre-release version requests require a minor version"
4241 );
4242 assert_eq!(
4243 VersionRequest::from_str("3.14.5rc1").unwrap(),
4244 VersionRequest::MajorMinorPatchPrerelease(
4245 3,
4246 14,
4247 5,
4248 Prerelease {
4249 kind: PrereleaseKind::Rc,
4250 number: 1
4251 },
4252 PythonVariant::Default
4253 ),
4254 "Pre-release version requests with a non-zero patch are allowed (e.g., `3.14.5rc1`)"
4255 );
4256 assert_eq!(
4257 VersionRequest::from_str("3.13.2rc1").unwrap(),
4258 VersionRequest::MajorMinorPatchPrerelease(
4259 3,
4260 13,
4261 2,
4262 Prerelease {
4263 kind: PrereleaseKind::Rc,
4264 number: 1
4265 },
4266 PythonVariant::Default
4267 )
4268 );
4269 assert!(
4270 matches!(
4271 VersionRequest::from_str("3.12-dev"),
4272 Err(Error::InvalidVersionRequest(_))
4273 ),
4274 "Development version segments are not allowed"
4275 );
4276 assert!(
4277 matches!(
4278 VersionRequest::from_str("3.12+local"),
4279 Err(Error::InvalidVersionRequest(_))
4280 ),
4281 "Local version segments are not allowed"
4282 );
4283 assert!(
4284 matches!(
4285 VersionRequest::from_str("3.12.post0"),
4286 Err(Error::InvalidVersionRequest(_))
4287 ),
4288 "Post version segments are not allowed"
4289 );
4290 assert!(
4291 matches!(
4293 VersionRequest::from_str("31000"),
4294 Err(Error::InvalidVersionRequest(_))
4295 )
4296 );
4297 assert_eq!(
4298 VersionRequest::from_str("3t").unwrap(),
4299 VersionRequest::Major(3, PythonVariant::Freethreaded)
4300 );
4301 assert_eq!(
4302 VersionRequest::from_str("313t").unwrap(),
4303 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4304 );
4305 assert_eq!(
4306 VersionRequest::from_str("3.13t").unwrap(),
4307 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded)
4308 );
4309 assert_eq!(
4310 VersionRequest::from_str(">=3.13t").unwrap(),
4311 VersionRequest::Range(
4312 VersionSpecifiers::from_str(">=3.13").unwrap(),
4313 PythonVariant::Freethreaded
4314 )
4315 );
4316 assert_eq!(
4317 VersionRequest::from_str(">=3.13").unwrap(),
4318 VersionRequest::Range(
4319 VersionSpecifiers::from_str(">=3.13").unwrap(),
4320 PythonVariant::Default
4321 )
4322 );
4323 assert_eq!(
4324 VersionRequest::from_str(">=3.12,<3.14t").unwrap(),
4325 VersionRequest::Range(
4326 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4327 PythonVariant::Freethreaded
4328 )
4329 );
4330 assert!(matches!(
4331 VersionRequest::from_str("3.13tt"),
4332 Err(Error::InvalidVersionRequest(_))
4333 ));
4334 assert!(matches!(
4335 VersionRequest::from_str("3.12²t"),
4336 Err(Error::InvalidVersionRequest(_))
4337 ));
4338
4339 assert_eq!(
4341 VersionRequest::from_str("==3.12").unwrap(),
4342 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4343 );
4344 assert_eq!(
4345 VersionRequest::from_str("==3.12.1").unwrap(),
4346 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4347 );
4348 }
4349
4350 #[test]
4351 fn version_request_from_specifiers() {
4352 assert_eq!(
4354 VersionRequest::from_specifiers(
4355 VersionSpecifiers::from_str("==3.12").unwrap(),
4356 PythonVariant::Default
4357 ),
4358 VersionRequest::MajorMinor(3, 12, PythonVariant::Default)
4359 );
4360 assert_eq!(
4361 VersionRequest::from_specifiers(
4362 VersionSpecifiers::from_str("==3.12.1").unwrap(),
4363 PythonVariant::Default
4364 ),
4365 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default)
4366 );
4367
4368 assert_eq!(
4370 VersionRequest::from_specifiers(
4371 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4372 PythonVariant::Default
4373 ),
4374 VersionRequest::Range(
4375 VersionSpecifiers::from_str("==3.12.*").unwrap(),
4376 PythonVariant::Default
4377 )
4378 );
4379
4380 assert_eq!(
4382 VersionRequest::from_specifiers(
4383 VersionSpecifiers::from_str(">=3.12").unwrap(),
4384 PythonVariant::Default
4385 ),
4386 VersionRequest::Range(
4387 VersionSpecifiers::from_str(">=3.12").unwrap(),
4388 PythonVariant::Default
4389 )
4390 );
4391
4392 assert_eq!(
4394 VersionRequest::from_specifiers(
4395 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4396 PythonVariant::Default
4397 ),
4398 VersionRequest::Range(
4399 VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(),
4400 PythonVariant::Default
4401 )
4402 );
4403 }
4404
4405 #[test]
4406 fn executable_names_from_request() {
4407 fn case(request: &str, expected: &[&str]) {
4408 let (implementation, version) = match PythonRequest::parse(request) {
4409 PythonRequest::Any => (None, VersionRequest::Any),
4410 PythonRequest::Default => (None, VersionRequest::Default),
4411 PythonRequest::Version(version) => (None, version),
4412 PythonRequest::ImplementationVersion(implementation, version) => {
4413 (Some(implementation), version)
4414 }
4415 PythonRequest::Implementation(implementation) => {
4416 (Some(implementation), VersionRequest::Default)
4417 }
4418 result => {
4419 panic!("Test cases should request versions or implementations; got {result:?}")
4420 }
4421 };
4422
4423 let result: Vec<_> = version
4424 .executable_names(implementation.as_ref())
4425 .into_iter()
4426 .map(|name| name.to_string())
4427 .collect();
4428
4429 let expected: Vec<_> = expected
4430 .iter()
4431 .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX))
4432 .collect();
4433
4434 assert_eq!(result, expected, "mismatch for case \"{request}\"");
4435 }
4436
4437 case(
4438 "any",
4439 &[
4440 "python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
4441 "pyodide", "pyodide3",
4442 ],
4443 );
4444
4445 case("default", &["python", "python3"]);
4446
4447 case("3", &["python3", "python"]);
4448
4449 case("4", &["python4", "python"]);
4450
4451 case("3.13", &["python3.13", "python3", "python"]);
4452
4453 case("pypy", &["pypy", "pypy3", "python", "python3"]);
4454
4455 case(
4456 "pypy@3.10",
4457 &[
4458 "pypy3.10",
4459 "pypy3",
4460 "pypy",
4461 "python3.10",
4462 "python3",
4463 "python",
4464 ],
4465 );
4466
4467 case(
4468 "3.13t",
4469 &[
4470 "python3.13t",
4471 "python3.13",
4472 "python3t",
4473 "python3",
4474 "pythont",
4475 "python",
4476 ],
4477 );
4478 case("3t", &["python3t", "python3", "pythont", "python"]);
4479
4480 case(
4481 "3.13.2",
4482 &["python3.13.2", "python3.13", "python3", "python"],
4483 );
4484
4485 case(
4486 "3.13rc2",
4487 &["python3.13rc2", "python3.13", "python3", "python"],
4488 );
4489 }
4490
4491 #[test]
4492 fn test_try_split_prefix_and_version() {
4493 assert!(matches!(
4494 PythonRequest::try_split_prefix_and_version("prefix", "prefix"),
4495 Ok(None),
4496 ));
4497 assert!(matches!(
4498 PythonRequest::try_split_prefix_and_version("prefix", "prefix3"),
4499 Ok(Some(_)),
4500 ));
4501 assert!(matches!(
4502 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3"),
4503 Ok(Some(_)),
4504 ));
4505 assert!(matches!(
4506 PythonRequest::try_split_prefix_and_version("prefix", "prefix3notaversion"),
4507 Ok(None),
4508 ));
4509 assert!(
4511 PythonRequest::try_split_prefix_and_version("prefix", "prefix@3notaversion").is_err()
4512 );
4513 assert!(PythonRequest::try_split_prefix_and_version("", "@3").is_err());
4515 }
4516
4517 #[test]
4518 fn version_request_as_pep440_version() {
4519 assert_eq!(VersionRequest::Default.as_pep440_version(), None);
4521 assert_eq!(VersionRequest::Any.as_pep440_version(), None);
4522 assert_eq!(
4523 VersionRequest::from_str(">=3.10")
4524 .unwrap()
4525 .as_pep440_version(),
4526 None
4527 );
4528
4529 assert_eq!(
4531 VersionRequest::Major(3, PythonVariant::Default).as_pep440_version(),
4532 Some(Version::from_str("3").unwrap())
4533 );
4534
4535 assert_eq!(
4537 VersionRequest::MajorMinor(3, 12, PythonVariant::Default).as_pep440_version(),
4538 Some(Version::from_str("3.12").unwrap())
4539 );
4540
4541 assert_eq!(
4543 VersionRequest::MajorMinorPatch(3, 12, 5, PythonVariant::Default).as_pep440_version(),
4544 Some(Version::from_str("3.12.5").unwrap())
4545 );
4546
4547 assert_eq!(
4549 VersionRequest::MajorMinorPrerelease(
4550 3,
4551 14,
4552 Prerelease {
4553 kind: PrereleaseKind::Alpha,
4554 number: 1
4555 },
4556 PythonVariant::Default
4557 )
4558 .as_pep440_version(),
4559 Some(Version::from_str("3.14.0a1").unwrap())
4560 );
4561 assert_eq!(
4562 VersionRequest::MajorMinorPrerelease(
4563 3,
4564 14,
4565 Prerelease {
4566 kind: PrereleaseKind::Beta,
4567 number: 2
4568 },
4569 PythonVariant::Default
4570 )
4571 .as_pep440_version(),
4572 Some(Version::from_str("3.14.0b2").unwrap())
4573 );
4574 assert_eq!(
4575 VersionRequest::MajorMinorPrerelease(
4576 3,
4577 13,
4578 Prerelease {
4579 kind: PrereleaseKind::Rc,
4580 number: 3
4581 },
4582 PythonVariant::Default
4583 )
4584 .as_pep440_version(),
4585 Some(Version::from_str("3.13.0rc3").unwrap())
4586 );
4587
4588 assert_eq!(
4590 VersionRequest::Major(3, PythonVariant::Freethreaded).as_pep440_version(),
4591 Some(Version::from_str("3").unwrap())
4592 );
4593 assert_eq!(
4594 VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded).as_pep440_version(),
4595 Some(Version::from_str("3.13").unwrap())
4596 );
4597 }
4598
4599 #[test]
4600 fn python_request_as_pep440_version() {
4601 assert_eq!(PythonRequest::Any.as_pep440_version(), None);
4603 assert_eq!(PythonRequest::Default.as_pep440_version(), None);
4604
4605 assert_eq!(
4607 PythonRequest::Version(VersionRequest::MajorMinor(3, 11, PythonVariant::Default))
4608 .as_pep440_version(),
4609 Some(Version::from_str("3.11").unwrap())
4610 );
4611
4612 assert_eq!(
4614 PythonRequest::ImplementationVersion(
4615 ImplementationName::CPython,
4616 VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default),
4617 )
4618 .as_pep440_version(),
4619 Some(Version::from_str("3.12.1").unwrap())
4620 );
4621
4622 assert_eq!(
4624 PythonRequest::Implementation(ImplementationName::CPython).as_pep440_version(),
4625 None
4626 );
4627
4628 assert_eq!(
4630 PythonRequest::parse("cpython-3.13.2").as_pep440_version(),
4631 Some(Version::from_str("3.13.2").unwrap())
4632 );
4633
4634 assert_eq!(
4636 PythonRequest::parse("cpython-macos-aarch64-none").as_pep440_version(),
4637 None
4638 );
4639
4640 assert_eq!(
4642 PythonRequest::Version(VersionRequest::from_str(">=3.10").unwrap()).as_pep440_version(),
4643 None
4644 );
4645 }
4646
4647 #[test]
4648 fn intersects_requires_python_exact() {
4649 let requires_python =
4650 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4651
4652 assert!(PythonRequest::parse("3.12").intersects_requires_python(&requires_python));
4653 assert!(!PythonRequest::parse("3.11").intersects_requires_python(&requires_python));
4654 }
4655
4656 #[test]
4657 fn intersects_requires_python_major() {
4658 let requires_python =
4659 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4660
4661 assert!(PythonRequest::parse("3").intersects_requires_python(&requires_python));
4663 assert!(!PythonRequest::parse("2").intersects_requires_python(&requires_python));
4665 }
4666
4667 #[test]
4668 fn intersects_requires_python_range() {
4669 let requires_python =
4670 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4671
4672 assert!(PythonRequest::parse(">=3.12,<3.13").intersects_requires_python(&requires_python));
4673 assert!(!PythonRequest::parse(">=3.10,<3.12").intersects_requires_python(&requires_python));
4674 }
4675
4676 #[test]
4677 fn intersects_requires_python_implementation_range() {
4678 let requires_python =
4679 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4680
4681 assert!(
4682 PythonRequest::parse("cpython@>=3.12,<3.13")
4683 .intersects_requires_python(&requires_python)
4684 );
4685 assert!(
4686 !PythonRequest::parse("cpython@>=3.10,<3.12")
4687 .intersects_requires_python(&requires_python)
4688 );
4689 }
4690
4691 #[test]
4692 fn intersects_requires_python_no_version() {
4693 let requires_python =
4694 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap());
4695
4696 assert!(PythonRequest::Any.intersects_requires_python(&requires_python));
4698 assert!(PythonRequest::Default.intersects_requires_python(&requires_python));
4699 assert!(
4700 PythonRequest::Implementation(ImplementationName::CPython)
4701 .intersects_requires_python(&requires_python)
4702 );
4703 }
4704}