Skip to main content

uv_configuration/
prerelease.rs

1use std::ops::{Deref, DerefMut};
2use std::str::FromStr;
3
4use rustc_hash::FxHashMap;
5use uv_normalize::PackageName;
6use uv_pep440::{Operator, VersionSpecifier};
7
8/// Return whether a specifier opts into prereleases under the explicit prerelease policy.
9///
10/// Excluding a prerelease, as in `!=1.0a1`, does not opt in.
11pub fn specifier_opts_into_prereleases(specifier: &VersionSpecifier) -> bool {
12    match specifier.operator() {
13        Operator::NotEqual | Operator::NotEqualStar => false,
14        Operator::Equal
15        | Operator::EqualStar
16        | Operator::ExactEqual
17        | Operator::TildeEqual
18        | Operator::LessThan
19        | Operator::LessThanEqual
20        | Operator::GreaterThan
21        | Operator::GreaterThanEqual => specifier.any_prerelease(),
22    }
23}
24
25#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
26#[serde(deny_unknown_fields, rename_all = "kebab-case")]
27#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
28#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
29pub enum PrereleaseMode {
30    /// Disallow all pre-release versions.
31    Disallow,
32
33    /// Allow all pre-release versions.
34    Allow,
35
36    /// Prefer stable versions, falling back to pre-release versions when necessary.
37    #[default]
38    IfNecessary,
39
40    /// Prefer stable versions for first-party packages with explicit pre-release specifiers,
41    /// falling back to pre-release versions when necessary. Disallow pre-release versions for all
42    /// other packages.
43    Explicit,
44
45    /// Deprecated alias for `if-necessary`.
46    #[deprecated(note = "use `if-necessary` instead")]
47    IfNecessaryOrExplicit,
48}
49
50#[allow(deprecated)]
51impl std::fmt::Display for PrereleaseMode {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::Disallow => write!(f, "disallow"),
55            Self::Allow => write!(f, "allow"),
56            Self::IfNecessary => write!(f, "if-necessary"),
57            Self::Explicit => write!(f, "explicit"),
58            Self::IfNecessaryOrExplicit => write!(f, "if-necessary-or-explicit"),
59        }
60    }
61}
62
63impl FromStr for PrereleaseMode {
64    type Err = String;
65
66    fn from_str(value: &str) -> Result<Self, Self::Err> {
67        match value {
68            "disallow" => Ok(Self::Disallow),
69            "allow" => Ok(Self::Allow),
70            "if-necessary" => Ok(Self::IfNecessary),
71            "explicit" => Ok(Self::Explicit),
72            #[allow(deprecated)]
73            "if-necessary-or-explicit" => Ok(Self::IfNecessaryOrExplicit),
74            _ => Err(format!(
75                "expected one of `disallow`, `allow`, `if-necessary`, `explicit`, or `if-necessary-or-explicit`, found `{value}`"
76            )),
77        }
78    }
79}
80
81/// A package-specific pre-release selection policy.
82#[derive(Debug, Clone, PartialEq, Eq)]
83#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
84pub struct PrereleasePackageEntry {
85    package: PackageName,
86    mode: PrereleaseMode,
87}
88
89impl FromStr for PrereleasePackageEntry {
90    type Err = String;
91
92    fn from_str(value: &str) -> Result<Self, Self::Err> {
93        let Some((package, mode)) = value.split_once('=') else {
94            return Err(format!(
95                "Invalid `prerelease-package` value `{value}`: expected format `PACKAGE=MODE`"
96            ));
97        };
98
99        let package = PackageName::from_str(package).map_err(|err| {
100            format!("Invalid `prerelease-package` package name `{package}`: {err}")
101        })?;
102        let mode = PrereleaseMode::from_str(mode)
103            .map_err(|err| format!("Invalid `prerelease-package` mode: {err}"))?;
104
105        Ok(Self { package, mode })
106    }
107}
108
109/// Pre-release selection policies that apply to individual packages.
110#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
111#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
112pub struct PrereleasePackage(FxHashMap<PackageName, PrereleaseMode>);
113
114impl Deref for PrereleasePackage {
115    type Target = FxHashMap<PackageName, PrereleaseMode>;
116
117    fn deref(&self) -> &Self::Target {
118        &self.0
119    }
120}
121
122impl DerefMut for PrereleasePackage {
123    fn deref_mut(&mut self) -> &mut Self::Target {
124        &mut self.0
125    }
126}
127
128impl FromIterator<PrereleasePackageEntry> for PrereleasePackage {
129    fn from_iter<T: IntoIterator<Item = PrereleasePackageEntry>>(iter: T) -> Self {
130        Self(
131            iter.into_iter()
132                .map(|entry| (entry.package, entry.mode))
133                .collect(),
134        )
135    }
136}
137
138impl IntoIterator for PrereleasePackage {
139    type Item = (PackageName, PrereleaseMode);
140    type IntoIter = std::collections::hash_map::IntoIter<PackageName, PrereleaseMode>;
141
142    fn into_iter(self) -> Self::IntoIter {
143        self.0.into_iter()
144    }
145}
146
147impl<'a> IntoIterator for &'a PrereleasePackage {
148    type Item = (&'a PackageName, &'a PrereleaseMode);
149    type IntoIter = std::collections::hash_map::Iter<'a, PackageName, PrereleaseMode>;
150
151    fn into_iter(self) -> Self::IntoIter {
152        self.0.iter()
153    }
154}
155
156impl PrereleasePackage {
157    /// Returns whether no package-specific policies are configured.
158    pub fn is_empty(&self) -> bool {
159        self.0.is_empty()
160    }
161}
162
163/// A pre-release selection policy that applies globally and to individual packages.
164#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
165#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
166pub struct Prerelease {
167    /// Global policy that applies to packages without a package-specific override.
168    pub global: PrereleaseMode,
169    /// Package-specific policies that override the global policy.
170    pub package: PrereleasePackage,
171}
172
173impl Prerelease {
174    /// Returns the effective pre-release selection policy for a package.
175    pub fn mode(&self, package: &PackageName) -> PrereleaseMode {
176        self.package.get(package).copied().unwrap_or(self.global)
177    }
178}