1use std::borrow::Cow;
2#[cfg(any(test, feature = "testing"))]
3use std::ops::BitOr;
4use std::sync::{Mutex, OnceLock};
5use std::{
6 fmt::{Debug, Display, Formatter},
7 str::FromStr,
8};
9
10use enumflags2::{BitFlags, bitflags};
11use thiserror::Error;
12use uv_macros::PreviewMetadata;
13use uv_warnings::warn_user_once;
14
15enum PreviewState {
17 Provisional(Preview),
18 Final(Preview),
19}
20
21enum PreviewMode {
24 Normal(Mutex<PreviewState>),
26 #[cfg(feature = "testing")]
28 Test(std::sync::RwLock<Option<Preview>>),
29}
30
31static PREVIEW: OnceLock<PreviewMode> = OnceLock::new();
32
33#[derive(Debug, Error)]
35pub enum PreviewError {
36 #[error("The preview configuration has already been finalized")]
38 AlreadyFinalized,
39
40 #[error("The preview configuration has not been initialized yet")]
42 NotInitialized,
43
44 #[cfg(feature = "testing")]
46 #[error("The preview configuration is in test mode and {}::{} cannot be used", module_path!(), .0)]
47 InTest(&'static str),
48}
49
50pub fn set(preview: Preview) -> Result<(), PreviewError> {
54 let mode = PREVIEW.get_or_init(|| {
55 PreviewMode::Normal(Mutex::new(PreviewState::Provisional(Preview::default())))
56 });
57 match mode {
58 PreviewMode::Normal(mutex) => {
59 let mut state = mutex.lock().unwrap();
62 match &*state {
63 PreviewState::Provisional(_) => {
64 *state = PreviewState::Provisional(preview);
65 Ok(())
66 }
67 PreviewState::Final(_) => Err(PreviewError::AlreadyFinalized),
68 }
69 }
70 #[cfg(feature = "testing")]
71 PreviewMode::Test(_) => Err(PreviewError::InTest("set")),
72 }
73}
74
75pub fn finalize() -> Result<(), PreviewError> {
76 match PREVIEW.get().ok_or(PreviewError::NotInitialized)? {
77 PreviewMode::Normal(mutex) => {
78 let mut state = mutex.lock().unwrap();
81 match &*state {
82 PreviewState::Provisional(preview) => {
83 *state = PreviewState::Final(*preview);
84 Ok(())
85 }
86 PreviewState::Final(_) => Err(PreviewError::AlreadyFinalized),
87 }
88 }
89 #[cfg(feature = "testing")]
90 PreviewMode::Test(_) => Err(PreviewError::InTest("finalize")),
91 }
92}
93
94fn get() -> Preview {
101 match PREVIEW.get() {
102 Some(PreviewMode::Normal(mutex)) => match *mutex.lock().unwrap() {
103 PreviewState::Provisional(preview) => preview,
104 PreviewState::Final(preview) => preview,
105 },
106 #[cfg(feature = "testing")]
107 Some(PreviewMode::Test(rwlock)) => {
108 assert!(
109 test::HELD.get(),
110 "The preview configuration is in test mode but the current thread does not hold a `FeaturesGuard`\nHint: Use `{}::test::with_features` to get a `FeaturesGuard` and hold it when testing functions which rely on the global preview state",
111 module_path!()
112 );
113 rwlock
117 .read()
118 .unwrap()
119 .expect("FeaturesGuard is held but preview value is not set")
120 }
121 #[cfg(feature = "testing")]
122 None => panic!(
123 "The preview configuration has not been initialized\nHint: Use `{}::init` or `{}::test::with_features` to initialize it",
124 module_path!(),
125 module_path!()
126 ),
127 #[cfg(not(feature = "testing"))]
128 None => panic!("The preview configuration has not been initialized"),
129 }
130}
131
132pub fn is_enabled(flag: PreviewFeature) -> bool {
134 get().is_enabled(flag)
135}
136
137#[cfg(feature = "testing")]
139pub mod test {
140 use super::{PREVIEW, Preview, PreviewMode};
141 use std::cell::Cell;
142 use std::sync::{Mutex, MutexGuard, RwLock};
143
144 static MUTEX: Mutex<()> = Mutex::new(());
148
149 thread_local! {
150 pub(crate) static HELD: Cell<bool> = const { Cell::new(false) };
156 }
157
158 #[derive(Debug)]
161 #[expect(unused)]
162 pub struct FeaturesGuard(MutexGuard<'static, ()>);
163
164 pub fn with_features(features: &[super::PreviewFeature]) -> FeaturesGuard {
176 assert!(
177 !HELD.get(),
178 "Additional calls to `{}::with_features` are not allowed while holding a `FeaturesGuard`",
179 module_path!()
180 );
181
182 let guard = match MUTEX.lock() {
183 Ok(guard) => guard,
184 Err(err) => err.into_inner(),
189 };
190
191 HELD.set(true);
192
193 let state = PREVIEW.get_or_init(|| PreviewMode::Test(RwLock::new(None)));
194 match state {
195 PreviewMode::Test(rwlock) => {
196 *rwlock.write().unwrap() = Some(Preview::new(features));
197 }
198 PreviewMode::Normal(_) => {
199 panic!(
200 "Cannot use `{}::with_features` after `uv_preview::init` has been called",
201 module_path!()
202 );
203 }
204 }
205 FeaturesGuard(guard)
206 }
207
208 impl Drop for FeaturesGuard {
209 fn drop(&mut self) {
210 HELD.set(false);
211
212 match PREVIEW.get().unwrap() {
213 PreviewMode::Test(rwlock) => {
214 *rwlock.write().unwrap() = None;
215 }
216 PreviewMode::Normal(_) => {
217 unreachable!("FeaturesGuard should not exist when in Normal mode");
218 }
219 }
220 }
221 }
222}
223
224#[bitflags]
225#[expect(
226 clippy::use_self,
227 reason = "enumflags2 refers to the enum by name when inferring bits"
228)]
229#[repr(u64)]
230#[derive(Debug, Clone, Copy, PartialEq, Eq, PreviewMetadata)]
231pub enum PreviewFeature {
232 PythonInstallDefault,
234 JsonOutput,
236 Pylock,
238 AddBounds,
240 PackageConflicts,
242 ExtraBuildDependencies,
244 DetectModuleConflicts,
247 #[preview(alias = "format")]
249 FormatCommand,
250 NativeAuth,
252 S3Endpoint,
254 CacheSize,
256 CachePhysicalSpace,
258 InitProjectFlag,
260 WorkspaceMetadata,
262 WorkspaceDir,
264 WorkspaceList,
266 SbomExport,
268 AuthHelper,
270 DirectPublish,
272 TargetWorkspaceDiscovery,
276 MetadataJson,
278 GcsEndpoint,
280 AdjustUlimit,
282 SpecialCondaEnvNames,
284 RelocatableEnvsDefault,
286 PublishRequireNormalized,
289 #[preview(alias = "audit")]
291 AuditCommand,
292 ProjectDirectoryMustExist,
296 IndexExcludeNewer,
298 AzureEndpoint,
300 TomlBackwardsCompatibility,
303 MalwareCheck,
306 VenvSafeClear,
309 #[preview(alias = "check")]
311 CheckCommand,
312 PackagedInit,
315 CentralizedProjectEnvs,
318 ToolInstallLocks,
321 WorkspaceListScripts,
323 NoDistutilsPatch,
326 IndexHashAlgorithm,
328 LockfileFormatCheck,
330 LockWithoutMetadata,
332}
333
334impl Display for PreviewFeature {
335 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
336 write!(f, "{}", self.as_str())
337 }
338}
339
340#[derive(Debug, Error, Clone)]
341#[error("Unknown feature flag")]
342pub struct PreviewFeatureParseError;
343
344impl FromStr for PreviewFeature {
345 type Err = PreviewFeatureParseError;
346
347 fn from_str(s: &str) -> Result<Self, Self::Err> {
348 Self::metadata()
349 .iter()
350 .find(|(feature, _, aliases)| feature.as_str() == s || aliases.contains(&s))
351 .map(|(feature, _, _)| *feature)
352 .ok_or(PreviewFeatureParseError)
353 }
354}
355
356#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
357#[error("preview feature name cannot be empty")]
358pub struct EmptyPreviewFeatureNameError;
359
360#[derive(Debug, Clone)]
362pub enum MaybePreviewFeature {
363 Known(PreviewFeature),
364 Unknown(String),
365}
366
367impl FromStr for MaybePreviewFeature {
368 type Err = EmptyPreviewFeatureNameError;
369
370 fn from_str(s: &str) -> Result<Self, Self::Err> {
371 let s = s.trim();
372 if s.is_empty() {
373 return Err(EmptyPreviewFeatureNameError);
374 }
375
376 Ok(match PreviewFeature::from_str(s) {
377 Ok(feature) => Self::Known(feature),
378 Err(_) => Self::Unknown(s.to_string()),
379 })
380 }
381}
382
383impl<'de> serde::Deserialize<'de> for MaybePreviewFeature {
384 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
385 where
386 D: serde::Deserializer<'de>,
387 {
388 let name: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?;
389 Self::from_str(&name).map_err(serde::de::Error::custom)
390 }
391}
392
393#[cfg(feature = "schemars")]
394impl schemars::JsonSchema for MaybePreviewFeature {
395 fn schema_name() -> Cow<'static, str> {
396 Cow::Borrowed("PreviewFeature")
397 }
398
399 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
400 let choices: Vec<&str> = BitFlags::<PreviewFeature>::all()
403 .iter()
404 .map(PreviewFeature::as_str)
405 .collect();
406 schemars::json_schema!({
407 "type": "string",
408 "anyOf": [
409 {
410 "enum": choices,
411 },
412 {
413 "pattern": "\\S",
414 },
415 ],
416 })
417 }
418}
419
420#[derive(Clone, Copy, PartialEq, Eq, Default)]
421pub struct Preview {
422 flags: BitFlags<PreviewFeature>,
423}
424
425impl Debug for Preview {
426 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
427 let flags: Vec<_> = self.flags.iter().collect();
428 f.debug_struct("Preview").field("flags", &flags).finish()
429 }
430}
431
432impl Preview {
433 #[cfg(any(test, feature = "testing"))]
434 fn new(flags: &[PreviewFeature]) -> Self {
435 Self {
436 flags: flags.iter().copied().fold(BitFlags::empty(), BitOr::bitor),
437 }
438 }
439
440 pub fn all() -> Self {
441 Self {
442 flags: BitFlags::all(),
443 }
444 }
445
446 pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
448 self.flags.contains(flag)
449 }
450
451 pub fn all_enabled(&self) -> bool {
453 self.flags.is_all()
454 }
455
456 pub fn any_enabled(&self) -> bool {
458 !self.flags.is_empty()
459 }
460
461 pub fn from_feature_names<'a>(
463 feature_names: impl IntoIterator<Item = &'a MaybePreviewFeature>,
464 ) -> Self {
465 let mut flags = BitFlags::empty();
466
467 for feature_name in feature_names {
468 match feature_name {
469 MaybePreviewFeature::Known(feature) => flags |= *feature,
470 MaybePreviewFeature::Unknown(feature_name) => {
471 warn_user_once!("Unknown preview feature: `{feature_name}`");
472 }
473 }
474 }
475
476 Self { flags }
477 }
478}
479
480impl Display for Preview {
481 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
482 if self.flags.is_empty() {
483 write!(f, "disabled")
484 } else if self.flags.is_all() {
485 write!(f, "enabled")
486 } else {
487 write!(
488 f,
489 "{}",
490 itertools::join(self.flags.iter().map(PreviewFeature::as_str), ",")
491 )
492 }
493 }
494}
495
496impl FromStr for Preview {
497 type Err = EmptyPreviewFeatureNameError;
498
499 fn from_str(s: &str) -> Result<Self, Self::Err> {
500 let feature_names = s
501 .split(',')
502 .map(MaybePreviewFeature::from_str)
503 .collect::<Result<Vec<_>, _>>()?;
504
505 Ok(Self::from_feature_names(&feature_names))
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn test_preview_feature_from_str() {
515 for &(feature, _, aliases) in PreviewFeature::metadata() {
516 assert_eq!(PreviewFeature::from_str(feature.as_str()).unwrap(), feature);
517
518 for &alias in aliases {
519 assert_eq!(PreviewFeature::from_str(alias).unwrap(), feature);
520 }
521 }
522 }
523
524 #[test]
525 fn test_preview_from_str() {
526 let preview = Preview::from_str("python-install-default").unwrap();
528 assert_eq!(preview.flags, PreviewFeature::PythonInstallDefault);
529
530 let preview = Preview::from_str("json-output,pylock").unwrap();
532 assert!(preview.is_enabled(PreviewFeature::JsonOutput));
533 assert!(preview.is_enabled(PreviewFeature::Pylock));
534 assert_eq!(preview.flags.bits().count_ones(), 2);
535
536 let preview = Preview::from_str("tool-install-locks").unwrap();
537 assert!(preview.is_enabled(PreviewFeature::ToolInstallLocks));
538
539 let preview = Preview::from_str("pylock , add-bounds").unwrap();
541 assert!(preview.is_enabled(PreviewFeature::Pylock));
542 assert!(preview.is_enabled(PreviewFeature::AddBounds));
543
544 assert_eq!(Preview::from_str(""), Err(EmptyPreviewFeatureNameError));
546 assert!(Preview::from_str("pylock,").is_err());
547 assert!(Preview::from_str(",pylock").is_err());
548
549 let preview = Preview::from_str("unknown-feature,pylock").unwrap();
551 assert!(preview.is_enabled(PreviewFeature::Pylock));
552 assert_eq!(preview.flags.bits().count_ones(), 1);
553 }
554
555 #[test]
556 fn test_preview_display() {
557 let preview = Preview::default();
559 assert_eq!(preview.to_string(), "disabled");
560 let preview = Preview::new(&[]);
561 assert_eq!(preview.to_string(), "disabled");
562
563 let preview = Preview::all();
565 assert_eq!(preview.to_string(), "enabled");
566
567 let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]);
569 assert_eq!(preview.to_string(), "python-install-default");
570
571 let preview = Preview::new(&[PreviewFeature::JsonOutput, PreviewFeature::Pylock]);
573 assert_eq!(preview.to_string(), "json-output,pylock");
574 }
575
576 #[test]
577 fn test_global_preview() {
578 {
579 let _guard =
580 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
581 assert!(!is_enabled(PreviewFeature::InitProjectFlag));
582 assert!(is_enabled(PreviewFeature::Pylock));
583 assert!(is_enabled(PreviewFeature::WorkspaceMetadata));
584 assert!(!is_enabled(PreviewFeature::AuthHelper));
585 }
586 {
587 let _guard =
588 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
589 assert!(is_enabled(PreviewFeature::InitProjectFlag));
590 assert!(!is_enabled(PreviewFeature::Pylock));
591 assert!(!is_enabled(PreviewFeature::WorkspaceMetadata));
592 assert!(is_enabled(PreviewFeature::AuthHelper));
593 }
594 }
595
596 #[test]
597 #[should_panic(
598 expected = "Additional calls to `uv_preview::test::with_features` are not allowed while holding a `FeaturesGuard`"
599 )]
600 fn test_global_preview_panic_nested() {
601 let _guard =
602 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
603 let _guard2 =
604 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
605 }
606
607 #[test]
608 #[should_panic(expected = "uv_preview::test::with_features")]
609 fn test_global_preview_panic_uninitialized() {
610 let _preview = get();
611 }
612}