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