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