1use std::{
2 fmt::{Debug, Display, Formatter},
3 ops::BitOr,
4 str::FromStr,
5};
6
7use enumflags2::{BitFlags, bitflags};
8use thiserror::Error;
9use uv_warnings::warn_user_once;
10
11#[bitflags]
12#[repr(u32)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PreviewFeature {
15 PythonInstallDefault = 1 << 0,
16 PythonUpgrade = 1 << 1,
17 JsonOutput = 1 << 2,
18 Pylock = 1 << 3,
19 AddBounds = 1 << 4,
20 PackageConflicts = 1 << 5,
21 ExtraBuildDependencies = 1 << 6,
22 DetectModuleConflicts = 1 << 7,
23 Format = 1 << 8,
24 NativeAuth = 1 << 9,
25 S3Endpoint = 1 << 10,
26 CacheSize = 1 << 11,
27 InitProjectFlag = 1 << 12,
28 WorkspaceMetadata = 1 << 13,
29 WorkspaceDir = 1 << 14,
30 WorkspaceList = 1 << 15,
31 SbomExport = 1 << 16,
32 AuthHelper = 1 << 17,
33 DirectPublish = 1 << 18,
34 TargetWorkspaceDiscovery = 1 << 19,
35 MetadataJson = 1 << 20,
36 GcsEndpoint = 1 << 21,
37 AdjustUlimit = 1 << 22,
38 SpecialCondaEnvNames = 1 << 23,
39 RelocatableEnvsDefault = 1 << 24,
40 PublishRequireNormalized = 1 << 25,
41 Audit = 1 << 26,
42}
43
44impl PreviewFeature {
45 fn as_str(self) -> &'static str {
47 match self {
48 Self::PythonInstallDefault => "python-install-default",
49 Self::PythonUpgrade => "python-upgrade",
50 Self::JsonOutput => "json-output",
51 Self::Pylock => "pylock",
52 Self::AddBounds => "add-bounds",
53 Self::PackageConflicts => "package-conflicts",
54 Self::ExtraBuildDependencies => "extra-build-dependencies",
55 Self::DetectModuleConflicts => "detect-module-conflicts",
56 Self::Format => "format",
57 Self::NativeAuth => "native-auth",
58 Self::S3Endpoint => "s3-endpoint",
59 Self::CacheSize => "cache-size",
60 Self::InitProjectFlag => "init-project-flag",
61 Self::WorkspaceMetadata => "workspace-metadata",
62 Self::WorkspaceDir => "workspace-dir",
63 Self::WorkspaceList => "workspace-list",
64 Self::SbomExport => "sbom-export",
65 Self::AuthHelper => "auth-helper",
66 Self::DirectPublish => "direct-publish",
67 Self::TargetWorkspaceDiscovery => "target-workspace-discovery",
68 Self::MetadataJson => "metadata-json",
69 Self::GcsEndpoint => "gcs-endpoint",
70 Self::AdjustUlimit => "adjust-ulimit",
71 Self::SpecialCondaEnvNames => "special-conda-env-names",
72 Self::RelocatableEnvsDefault => "relocatable-envs-default",
73 Self::PublishRequireNormalized => "publish-require-normalized",
74 Self::Audit => "audit",
75 }
76 }
77}
78
79impl Display for PreviewFeature {
80 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
81 write!(f, "{}", self.as_str())
82 }
83}
84
85#[derive(Debug, Error, Clone)]
86#[error("Unknown feature flag")]
87pub struct PreviewFeatureParseError;
88
89impl FromStr for PreviewFeature {
90 type Err = PreviewFeatureParseError;
91
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
93 Ok(match s {
94 "python-install-default" => Self::PythonInstallDefault,
95 "python-upgrade" => Self::PythonUpgrade,
96 "json-output" => Self::JsonOutput,
97 "pylock" => Self::Pylock,
98 "add-bounds" => Self::AddBounds,
99 "package-conflicts" => Self::PackageConflicts,
100 "extra-build-dependencies" => Self::ExtraBuildDependencies,
101 "detect-module-conflicts" => Self::DetectModuleConflicts,
102 "format" => Self::Format,
103 "native-auth" => Self::NativeAuth,
104 "s3-endpoint" => Self::S3Endpoint,
105 "gcs-endpoint" => Self::GcsEndpoint,
106 "cache-size" => Self::CacheSize,
107 "init-project-flag" => Self::InitProjectFlag,
108 "workspace-metadata" => Self::WorkspaceMetadata,
109 "workspace-dir" => Self::WorkspaceDir,
110 "workspace-list" => Self::WorkspaceList,
111 "sbom-export" => Self::SbomExport,
112 "auth-helper" => Self::AuthHelper,
113 "direct-publish" => Self::DirectPublish,
114 "target-workspace-discovery" => Self::TargetWorkspaceDiscovery,
115 "metadata-json" => Self::MetadataJson,
116 "adjust-ulimit" => Self::AdjustUlimit,
117 "special-conda-env-names" => Self::SpecialCondaEnvNames,
118 "relocatable-envs-default" => Self::RelocatableEnvsDefault,
119 "publish-require-normalized" => Self::PublishRequireNormalized,
120 "audit" => Self::Audit,
121 _ => return Err(PreviewFeatureParseError),
122 })
123 }
124}
125
126#[derive(Clone, Copy, PartialEq, Eq, Default)]
127pub struct Preview {
128 flags: BitFlags<PreviewFeature>,
129}
130
131impl Debug for Preview {
132 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
133 let flags: Vec<_> = self.flags.iter().collect();
134 f.debug_struct("Preview").field("flags", &flags).finish()
135 }
136}
137
138impl Preview {
139 pub fn new(flags: &[PreviewFeature]) -> Self {
140 Self {
141 flags: flags.iter().copied().fold(BitFlags::empty(), BitOr::bitor),
142 }
143 }
144
145 pub fn all() -> Self {
146 Self {
147 flags: BitFlags::all(),
148 }
149 }
150
151 pub fn from_args(preview: bool, no_preview: bool, preview_features: &[PreviewFeature]) -> Self {
152 if no_preview {
153 return Self::default();
154 }
155
156 if preview {
157 return Self::all();
158 }
159
160 Self::new(preview_features)
161 }
162
163 pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
165 self.flags.contains(flag)
166 }
167
168 pub fn all_enabled(&self) -> bool {
170 self.flags.is_all()
171 }
172
173 pub fn any_enabled(&self) -> bool {
175 !self.flags.is_empty()
176 }
177}
178
179impl Display for Preview {
180 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
181 if self.flags.is_empty() {
182 write!(f, "disabled")
183 } else if self.flags.is_all() {
184 write!(f, "enabled")
185 } else {
186 write!(
187 f,
188 "{}",
189 itertools::join(self.flags.iter().map(PreviewFeature::as_str), ",")
190 )
191 }
192 }
193}
194
195#[derive(Debug, Error, Clone)]
196pub enum PreviewParseError {
197 #[error("Empty string in preview features: {0}")]
198 Empty(String),
199}
200
201impl FromStr for Preview {
202 type Err = PreviewParseError;
203
204 fn from_str(s: &str) -> Result<Self, Self::Err> {
205 let mut flags = BitFlags::empty();
206
207 for part in s.split(',') {
208 let part = part.trim();
209 if part.is_empty() {
210 return Err(PreviewParseError::Empty(
211 "Empty string in preview features".to_string(),
212 ));
213 }
214
215 match PreviewFeature::from_str(part) {
216 Ok(flag) => flags |= flag,
217 Err(_) => {
218 warn_user_once!("Unknown preview feature: `{part}`");
219 }
220 }
221 }
222
223 Ok(Self { flags })
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_preview_feature_from_str() {
233 let features = PreviewFeature::from_str("python-install-default").unwrap();
234 assert_eq!(features, PreviewFeature::PythonInstallDefault);
235 }
236
237 #[test]
238 fn test_preview_from_str() {
239 let preview = Preview::from_str("python-install-default").unwrap();
241 assert_eq!(preview.flags, PreviewFeature::PythonInstallDefault);
242
243 let preview = Preview::from_str("python-upgrade,json-output").unwrap();
245 assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
246 assert!(preview.is_enabled(PreviewFeature::JsonOutput));
247 assert_eq!(preview.flags.bits().count_ones(), 2);
248
249 let preview = Preview::from_str("pylock , add-bounds").unwrap();
251 assert!(preview.is_enabled(PreviewFeature::Pylock));
252 assert!(preview.is_enabled(PreviewFeature::AddBounds));
253
254 assert!(Preview::from_str("").is_err());
256 assert!(Preview::from_str("pylock,").is_err());
257 assert!(Preview::from_str(",pylock").is_err());
258
259 let preview = Preview::from_str("unknown-feature,pylock").unwrap();
261 assert!(preview.is_enabled(PreviewFeature::Pylock));
262 assert_eq!(preview.flags.bits().count_ones(), 1);
263 }
264
265 #[test]
266 fn test_preview_display() {
267 let preview = Preview::default();
269 assert_eq!(preview.to_string(), "disabled");
270 let preview = Preview::new(&[]);
271 assert_eq!(preview.to_string(), "disabled");
272
273 let preview = Preview::all();
275 assert_eq!(preview.to_string(), "enabled");
276
277 let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]);
279 assert_eq!(preview.to_string(), "python-install-default");
280
281 let preview = Preview::new(&[PreviewFeature::PythonUpgrade, PreviewFeature::Pylock]);
283 assert_eq!(preview.to_string(), "python-upgrade,pylock");
284 }
285
286 #[test]
287 fn test_preview_from_args() {
288 let preview = Preview::from_args(false, false, &[]);
290 assert_eq!(preview.to_string(), "disabled");
291
292 let preview = Preview::from_args(true, true, &[]);
294 assert_eq!(preview.to_string(), "disabled");
295
296 let preview = Preview::from_args(true, false, &[]);
298 assert_eq!(preview.to_string(), "enabled");
299
300 let features = vec![PreviewFeature::PythonUpgrade, PreviewFeature::JsonOutput];
302 let preview = Preview::from_args(false, false, &features);
303 assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
304 assert!(preview.is_enabled(PreviewFeature::JsonOutput));
305 assert!(!preview.is_enabled(PreviewFeature::Pylock));
306 }
307
308 #[test]
309 fn test_preview_feature_as_str() {
310 assert_eq!(
311 PreviewFeature::PythonInstallDefault.as_str(),
312 "python-install-default"
313 );
314 assert_eq!(PreviewFeature::PythonUpgrade.as_str(), "python-upgrade");
315 assert_eq!(PreviewFeature::JsonOutput.as_str(), "json-output");
316 assert_eq!(PreviewFeature::Pylock.as_str(), "pylock");
317 assert_eq!(PreviewFeature::AddBounds.as_str(), "add-bounds");
318 assert_eq!(
319 PreviewFeature::PackageConflicts.as_str(),
320 "package-conflicts"
321 );
322 assert_eq!(
323 PreviewFeature::ExtraBuildDependencies.as_str(),
324 "extra-build-dependencies"
325 );
326 assert_eq!(
327 PreviewFeature::DetectModuleConflicts.as_str(),
328 "detect-module-conflicts"
329 );
330 assert_eq!(PreviewFeature::Format.as_str(), "format");
331 assert_eq!(PreviewFeature::NativeAuth.as_str(), "native-auth");
332 assert_eq!(PreviewFeature::S3Endpoint.as_str(), "s3-endpoint");
333 assert_eq!(PreviewFeature::CacheSize.as_str(), "cache-size");
334 assert_eq!(
335 PreviewFeature::InitProjectFlag.as_str(),
336 "init-project-flag"
337 );
338 assert_eq!(
339 PreviewFeature::WorkspaceMetadata.as_str(),
340 "workspace-metadata"
341 );
342 assert_eq!(PreviewFeature::WorkspaceDir.as_str(), "workspace-dir");
343 assert_eq!(PreviewFeature::WorkspaceList.as_str(), "workspace-list");
344 assert_eq!(PreviewFeature::SbomExport.as_str(), "sbom-export");
345 assert_eq!(PreviewFeature::AuthHelper.as_str(), "auth-helper");
346 assert_eq!(PreviewFeature::DirectPublish.as_str(), "direct-publish");
347 assert_eq!(
348 PreviewFeature::TargetWorkspaceDiscovery.as_str(),
349 "target-workspace-discovery"
350 );
351 assert_eq!(PreviewFeature::MetadataJson.as_str(), "metadata-json");
352 assert_eq!(PreviewFeature::GcsEndpoint.as_str(), "gcs-endpoint");
353 assert_eq!(PreviewFeature::AdjustUlimit.as_str(), "adjust-ulimit");
354 assert_eq!(
355 PreviewFeature::SpecialCondaEnvNames.as_str(),
356 "special-conda-env-names"
357 );
358 assert_eq!(
359 PreviewFeature::RelocatableEnvsDefault.as_str(),
360 "relocatable-envs-default"
361 );
362 assert_eq!(
363 PreviewFeature::PublishRequireNormalized.as_str(),
364 "publish-require-normalized"
365 );
366 }
367}