Skip to main content

uv_resolver/
prerelease.rs

1use std::borrow::Cow;
2
3use uv_distribution_types::{Requirement, RequirementSource};
4use uv_normalize::PackageName;
5use uv_pep440::{Operator, VersionSpecifiers};
6
7use crate::resolver::ForkSet;
8use crate::{DependencyMode, Manifest, ResolverEnvironment};
9
10#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11#[serde(deny_unknown_fields, rename_all = "kebab-case")]
12#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
13#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
14pub enum PrereleaseMode {
15    /// Disallow all pre-release versions.
16    Disallow,
17
18    /// Allow all pre-release versions.
19    Allow,
20
21    /// Prefer stable versions, falling back to pre-release versions when necessary.
22    #[default]
23    IfNecessary,
24
25    /// Prefer stable versions for first-party packages with explicit pre-release specifiers,
26    /// falling back to pre-release versions when necessary. Disallow pre-release versions for all
27    /// other packages.
28    Explicit,
29
30    /// Deprecated alias for `if-necessary`.
31    #[deprecated(note = "use `if-necessary` instead")]
32    IfNecessaryOrExplicit,
33}
34
35#[allow(deprecated)]
36impl std::fmt::Display for PrereleaseMode {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            Self::Disallow => write!(f, "disallow"),
40            Self::Allow => write!(f, "allow"),
41            Self::IfNecessary => write!(f, "if-necessary"),
42            Self::Explicit => write!(f, "explicit"),
43            Self::IfNecessaryOrExplicit => write!(f, "if-necessary-or-explicit"),
44        }
45    }
46}
47
48/// Like [`PrereleaseMode`], but with any additional information required to select a candidate,
49/// like the set of direct dependencies.
50#[derive(Debug, Clone)]
51pub(crate) enum PrereleaseStrategy {
52    /// Disallow all pre-release versions.
53    Disallow,
54
55    /// Allow all pre-release versions.
56    Allow,
57
58    /// Prefer stable versions, falling back to pre-release versions when necessary.
59    IfNecessary,
60
61    /// Prefer stable versions for first-party packages with explicit pre-release specifiers,
62    /// falling back to pre-release versions when necessary. Disallow pre-release versions for all
63    /// other packages.
64    Explicit(ForkSet),
65}
66
67impl PrereleaseStrategy {
68    #[allow(deprecated)]
69    pub(crate) fn from_mode(
70        mode: PrereleaseMode,
71        manifest: &Manifest,
72        env: &ResolverEnvironment,
73        dependencies: DependencyMode,
74    ) -> Self {
75        match mode {
76            PrereleaseMode::Disallow => Self::Disallow,
77            PrereleaseMode::Allow => Self::Allow,
78            PrereleaseMode::IfNecessary | PrereleaseMode::IfNecessaryOrExplicit => {
79                Self::IfNecessary
80            }
81            PrereleaseMode::Explicit => Self::Explicit(Self::explicit_packages(
82                manifest.candidate_selection_requirements(env, dependencies),
83            )),
84        }
85    }
86
87    fn explicit_packages<'a>(requirements: impl Iterator<Item = Cow<'a, Requirement>>) -> ForkSet {
88        let mut packages = ForkSet::default();
89        for requirement in requirements {
90            let RequirementSource::Registry { specifier, .. } = &requirement.source else {
91                continue;
92            };
93
94            if contains_prerelease(specifier) {
95                packages.add(&requirement, ());
96            }
97        }
98        packages
99    }
100
101    /// Returns the pre-release candidate selection policy for a package.
102    ///
103    /// Pre-releases remain in the candidate universe but, unless they are globally allowed, are
104    /// considered only after stable candidates. Keeping the candidate universe fixed is required
105    /// for PubGrub's learned incompatibilities to remain valid.
106    pub(crate) fn selection(
107        &self,
108        package_name: &PackageName,
109        env: &ResolverEnvironment,
110    ) -> PrereleaseSelection {
111        match self {
112            Self::Disallow => PrereleaseSelection::Disallow,
113            Self::Allow => PrereleaseSelection::Allow,
114            Self::IfNecessary => PrereleaseSelection::PreferStable,
115            Self::Explicit(packages) => {
116                if packages.contains(package_name, env) {
117                    PrereleaseSelection::PreferStable
118                } else {
119                    PrereleaseSelection::Disallow
120                }
121            }
122        }
123    }
124}
125
126/// Returns `true` if the specifiers explicitly mention a pre-release version.
127///
128/// Exclusions do not opt a package into pre-releases. For example, `!=1.0a1` should not change
129/// which candidate kinds are considered.
130fn contains_prerelease(specifiers: &VersionSpecifiers) -> bool {
131    specifiers
132        .iter()
133        .filter(|specifier| {
134            !matches!(
135                specifier.operator(),
136                Operator::NotEqual | Operator::NotEqualStar
137            )
138        })
139        .any(uv_pep440::VersionSpecifier::any_prerelease)
140}
141
142/// How pre-release candidates participate in version selection.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub(crate) enum PrereleaseSelection {
145    /// Do not consider pre-release candidates.
146    Disallow,
147    /// Consider stable and pre-release candidates in normal version order.
148    Allow,
149    /// Prefer stable candidates, falling back to pre-releases only after stable candidates are
150    /// exhausted.
151    PreferStable,
152}