1use std::borrow::Cow;
2use std::ops::{Deref, DerefMut};
3use std::str::FromStr;
4
5use rustc_hash::FxHashMap;
6use uv_distribution_types::{Requirement, RequirementSource};
7use uv_normalize::PackageName;
8use uv_pep440::{Operator, VersionSpecifiers};
9
10use crate::resolver::ForkSet;
11use crate::{DependencyMode, Manifest, ResolverEnvironment};
12
13#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14#[serde(deny_unknown_fields, rename_all = "kebab-case")]
15#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
16#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
17pub enum PrereleaseMode {
18 Disallow,
20
21 Allow,
23
24 #[default]
26 IfNecessary,
27
28 Explicit,
32
33 #[deprecated(note = "use `if-necessary` instead")]
35 IfNecessaryOrExplicit,
36}
37
38#[allow(deprecated)]
39impl std::fmt::Display for PrereleaseMode {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::Disallow => write!(f, "disallow"),
43 Self::Allow => write!(f, "allow"),
44 Self::IfNecessary => write!(f, "if-necessary"),
45 Self::Explicit => write!(f, "explicit"),
46 Self::IfNecessaryOrExplicit => write!(f, "if-necessary-or-explicit"),
47 }
48 }
49}
50
51impl FromStr for PrereleaseMode {
52 type Err = String;
53
54 fn from_str(value: &str) -> Result<Self, Self::Err> {
55 match value {
56 "disallow" => Ok(Self::Disallow),
57 "allow" => Ok(Self::Allow),
58 "if-necessary" => Ok(Self::IfNecessary),
59 "explicit" => Ok(Self::Explicit),
60 #[allow(deprecated)]
61 "if-necessary-or-explicit" => Ok(Self::IfNecessaryOrExplicit),
62 _ => Err(format!(
63 "expected one of `disallow`, `allow`, `if-necessary`, `explicit`, or `if-necessary-or-explicit`, found `{value}`"
64 )),
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
72pub struct PrereleasePackageEntry {
73 package: PackageName,
74 mode: PrereleaseMode,
75}
76
77impl FromStr for PrereleasePackageEntry {
78 type Err = String;
79
80 fn from_str(value: &str) -> Result<Self, Self::Err> {
81 let Some((package, mode)) = value.split_once('=') else {
82 return Err(format!(
83 "Invalid `prerelease-package` value `{value}`: expected format `PACKAGE=MODE`"
84 ));
85 };
86
87 let package = PackageName::from_str(package).map_err(|err| {
88 format!("Invalid `prerelease-package` package name `{package}`: {err}")
89 })?;
90 let mode = PrereleaseMode::from_str(mode)
91 .map_err(|err| format!("Invalid `prerelease-package` mode: {err}"))?;
92
93 Ok(Self { package, mode })
94 }
95}
96
97#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
99#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
100pub struct PrereleasePackage(FxHashMap<PackageName, PrereleaseMode>);
101
102impl Deref for PrereleasePackage {
103 type Target = FxHashMap<PackageName, PrereleaseMode>;
104
105 fn deref(&self) -> &Self::Target {
106 &self.0
107 }
108}
109
110impl DerefMut for PrereleasePackage {
111 fn deref_mut(&mut self) -> &mut Self::Target {
112 &mut self.0
113 }
114}
115
116impl FromIterator<PrereleasePackageEntry> for PrereleasePackage {
117 fn from_iter<T: IntoIterator<Item = PrereleasePackageEntry>>(iter: T) -> Self {
118 Self(
119 iter.into_iter()
120 .map(|entry| (entry.package, entry.mode))
121 .collect(),
122 )
123 }
124}
125
126impl IntoIterator for PrereleasePackage {
127 type Item = (PackageName, PrereleaseMode);
128 type IntoIter = std::collections::hash_map::IntoIter<PackageName, PrereleaseMode>;
129
130 fn into_iter(self) -> Self::IntoIter {
131 self.0.into_iter()
132 }
133}
134
135impl<'a> IntoIterator for &'a PrereleasePackage {
136 type Item = (&'a PackageName, &'a PrereleaseMode);
137 type IntoIter = std::collections::hash_map::Iter<'a, PackageName, PrereleaseMode>;
138
139 fn into_iter(self) -> Self::IntoIter {
140 self.0.iter()
141 }
142}
143
144impl PrereleasePackage {
145 pub(crate) fn is_empty(&self) -> bool {
147 self.0.is_empty()
148 }
149}
150
151#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
153#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
154pub struct Prerelease {
155 pub global: PrereleaseMode,
157 pub package: PrereleasePackage,
159}
160
161impl Prerelease {
162 pub fn mode(&self, package: &PackageName) -> PrereleaseMode {
164 self.package.get(package).copied().unwrap_or(self.global)
165 }
166}
167
168#[derive(Debug, Clone)]
171pub(crate) struct PrereleaseStrategy {
172 default: PrereleasePolicy,
173 package: FxHashMap<PackageName, PrereleasePolicy>,
174}
175
176#[derive(Debug, Clone)]
177enum PrereleasePolicy {
178 Disallow,
180
181 Allow,
183
184 IfNecessary,
186
187 Explicit(ForkSet),
191}
192
193impl PrereleaseStrategy {
194 #[allow(deprecated)]
195 pub(crate) fn from_prerelease(
196 prerelease: &Prerelease,
197 manifest: &Manifest,
198 env: &ResolverEnvironment,
199 dependencies: DependencyMode,
200 ) -> Self {
201 Self {
202 default: Self::policy(prerelease.global, manifest, env, dependencies),
203 package: prerelease
204 .package
205 .iter()
206 .map(|(name, mode)| {
207 (
208 name.clone(),
209 Self::policy(*mode, manifest, env, dependencies),
210 )
211 })
212 .collect(),
213 }
214 }
215
216 #[allow(deprecated)]
217 fn policy(
218 mode: PrereleaseMode,
219 manifest: &Manifest,
220 env: &ResolverEnvironment,
221 dependencies: DependencyMode,
222 ) -> PrereleasePolicy {
223 match mode {
224 PrereleaseMode::Disallow => PrereleasePolicy::Disallow,
225 PrereleaseMode::Allow => PrereleasePolicy::Allow,
226 PrereleaseMode::IfNecessary | PrereleaseMode::IfNecessaryOrExplicit => {
227 PrereleasePolicy::IfNecessary
228 }
229 PrereleaseMode::Explicit => PrereleasePolicy::Explicit(Self::explicit_packages(
230 manifest.candidate_selection_requirements(env, dependencies),
231 )),
232 }
233 }
234
235 fn explicit_packages<'a>(requirements: impl Iterator<Item = Cow<'a, Requirement>>) -> ForkSet {
236 let mut packages = ForkSet::default();
237 for requirement in requirements {
238 let RequirementSource::Registry { specifier, .. } = &requirement.source else {
239 continue;
240 };
241
242 if contains_prerelease(specifier) {
243 packages.add(&requirement, ());
244 }
245 }
246 packages
247 }
248
249 pub(crate) fn selection(
255 &self,
256 package_name: &PackageName,
257 env: &ResolverEnvironment,
258 ) -> PrereleaseSelection {
259 match self.package.get(package_name).unwrap_or(&self.default) {
260 PrereleasePolicy::Disallow => PrereleaseSelection::Disallow,
261 PrereleasePolicy::Allow => PrereleaseSelection::Allow,
262 PrereleasePolicy::IfNecessary => PrereleaseSelection::PreferStable,
263 PrereleasePolicy::Explicit(packages) => {
264 if packages.contains(package_name, env) {
265 PrereleaseSelection::PreferStable
266 } else {
267 PrereleaseSelection::Disallow
268 }
269 }
270 }
271 }
272}
273
274fn contains_prerelease(specifiers: &VersionSpecifiers) -> bool {
279 specifiers
280 .iter()
281 .filter(|specifier| {
282 !matches!(
283 specifier.operator(),
284 Operator::NotEqual | Operator::NotEqualStar
285 )
286 })
287 .any(uv_pep440::VersionSpecifier::any_prerelease)
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub(crate) enum PrereleaseSelection {
293 Disallow,
295 Allow,
297 PreferStable,
300}