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