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 TargetWorkspaceDiscovery,
274 MetadataJson,
276 GcsEndpoint,
278 AdjustUlimit,
280 SpecialCondaEnvNames,
282 RelocatableEnvsDefault,
284 PublishRequireNormalized,
287 #[preview(alias = "audit")]
289 AuditCommand,
290 ProjectDirectoryMustExist,
294 IndexExcludeNewer,
296 AzureEndpoint,
298 TomlBackwardsCompatibility,
301 MalwareCheck,
304 VenvSafeClear,
307 #[preview(alias = "check")]
309 CheckCommand,
310 PackagedInit,
313 CentralizedProjectEnvs,
316 ToolInstallLocks,
319 WorkspaceListScripts,
321 NoDistutilsPatch,
324 IndexHashAlgorithm,
326 LockfileFormatCheck,
328 LockWithoutMetadata,
330 TarCodec,
332 IndexByName,
334 ArtifactHashFiltering,
336 ContentAddressedCache,
338 MissingExcludeNewerPackageLock,
341 ResolutionInputs,
344 BatchExport,
346 MinimumLibcVersion,
348 BuildDependencyCheck,
350 BuildLazyImports,
353}
354
355impl Display for PreviewFeature {
356 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
357 write!(f, "{}", self.as_str())
358 }
359}
360
361#[derive(Debug, Error, Clone)]
362#[error("Unknown feature flag")]
363pub struct PreviewFeatureParseError;
364
365impl FromStr for PreviewFeature {
366 type Err = PreviewFeatureParseError;
367
368 fn from_str(s: &str) -> Result<Self, Self::Err> {
369 Self::metadata()
370 .iter()
371 .find(|(feature, _, aliases)| feature.as_str() == s || aliases.contains(&s))
372 .map(|(feature, _, _)| *feature)
373 .ok_or(PreviewFeatureParseError)
374 }
375}
376
377#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
378#[error("preview feature name cannot be empty")]
379pub struct EmptyPreviewFeatureNameError;
380
381#[derive(Debug, Clone)]
383pub enum MaybePreviewFeature {
384 Known(PreviewFeature),
385 Unknown(String),
386}
387
388impl FromStr for MaybePreviewFeature {
389 type Err = EmptyPreviewFeatureNameError;
390
391 fn from_str(s: &str) -> Result<Self, Self::Err> {
392 let s = s.trim();
393 if s.is_empty() {
394 return Err(EmptyPreviewFeatureNameError);
395 }
396
397 Ok(match PreviewFeature::from_str(s) {
398 Ok(feature) => Self::Known(feature),
399 Err(_) => Self::Unknown(s.to_string()),
400 })
401 }
402}
403
404impl<'de> serde::Deserialize<'de> for MaybePreviewFeature {
405 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
406 where
407 D: serde::Deserializer<'de>,
408 {
409 let name: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?;
410 Self::from_str(&name).map_err(serde::de::Error::custom)
411 }
412}
413
414#[cfg(feature = "schemars")]
415impl schemars::JsonSchema for MaybePreviewFeature {
416 fn schema_name() -> Cow<'static, str> {
417 Cow::Borrowed("PreviewFeature")
418 }
419
420 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
421 let choices: Vec<&str> = BitFlags::<PreviewFeature>::all()
424 .iter()
425 .map(PreviewFeature::as_str)
426 .collect();
427 schemars::json_schema!({
428 "type": "string",
429 "anyOf": [
430 {
431 "enum": choices,
432 },
433 {
434 "pattern": "\\S",
435 },
436 ],
437 })
438 }
439}
440
441#[derive(Clone, Copy, PartialEq, Eq, Default)]
442pub struct Preview {
443 flags: BitFlags<PreviewFeature>,
444}
445
446impl Debug for Preview {
447 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
448 let flags: Vec<_> = self.flags.iter().collect();
449 f.debug_struct("Preview").field("flags", &flags).finish()
450 }
451}
452
453impl Preview {
454 #[cfg(any(test, feature = "testing"))]
455 fn new(flags: &[PreviewFeature]) -> Self {
456 Self {
457 flags: flags.iter().copied().fold(BitFlags::empty(), BitOr::bitor),
458 }
459 }
460
461 pub fn all() -> Self {
462 Self {
463 flags: BitFlags::all(),
464 }
465 }
466
467 pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
469 self.flags.contains(flag)
470 }
471
472 pub fn all_enabled(&self) -> bool {
474 self.flags.is_all()
475 }
476
477 pub fn any_enabled(&self) -> bool {
479 !self.flags.is_empty()
480 }
481
482 pub fn from_feature_names<'a>(
484 feature_names: impl IntoIterator<Item = &'a MaybePreviewFeature>,
485 ) -> Self {
486 let mut flags = BitFlags::empty();
487
488 for feature_name in feature_names {
489 match feature_name {
490 MaybePreviewFeature::Known(feature) => flags |= *feature,
491 MaybePreviewFeature::Unknown(feature_name) => {
492 warn_user_once!("Unknown preview feature: `{feature_name}`");
493 }
494 }
495 }
496
497 Self { flags }
498 }
499}
500
501impl Display for Preview {
502 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
503 if self.flags.is_empty() {
504 write!(f, "disabled")
505 } else if self.flags.is_all() {
506 write!(f, "enabled")
507 } else {
508 write!(
509 f,
510 "{}",
511 itertools::join(self.flags.iter().map(PreviewFeature::as_str), ",")
512 )
513 }
514 }
515}
516
517impl FromStr for Preview {
518 type Err = EmptyPreviewFeatureNameError;
519
520 fn from_str(s: &str) -> Result<Self, Self::Err> {
521 let feature_names = s
522 .split(',')
523 .map(MaybePreviewFeature::from_str)
524 .collect::<Result<Vec<_>, _>>()?;
525
526 Ok(Self::from_feature_names(&feature_names))
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_preview_feature_from_str() {
536 for &(feature, _, aliases) in PreviewFeature::metadata() {
537 assert_eq!(PreviewFeature::from_str(feature.as_str()).unwrap(), feature);
538
539 for &alias in aliases {
540 assert_eq!(PreviewFeature::from_str(alias).unwrap(), feature);
541 }
542 }
543
544 let feature = PreviewFeature::from_str("tar-codec").unwrap();
545 assert_eq!(feature, PreviewFeature::TarCodec);
546 assert_eq!(feature.to_string(), "tar-codec");
547 }
548
549 #[test]
550 fn test_preview_from_str() {
551 let preview = Preview::from_str("python-install-default").unwrap();
553 assert_eq!(preview.flags, PreviewFeature::PythonInstallDefault);
554
555 let preview = Preview::from_str("tar-codec").unwrap();
556 assert!(preview.is_enabled(PreviewFeature::TarCodec));
557
558 let preview = Preview::from_str("json-output,pylock").unwrap();
560 assert!(preview.is_enabled(PreviewFeature::JsonOutput));
561 assert!(preview.is_enabled(PreviewFeature::Pylock));
562 assert_eq!(preview.flags.bits().count_ones(), 2);
563
564 let preview = Preview::from_str("tool-install-locks").unwrap();
565 assert!(preview.is_enabled(PreviewFeature::ToolInstallLocks));
566
567 let preview = Preview::from_str("pylock , add-bounds").unwrap();
569 assert!(preview.is_enabled(PreviewFeature::Pylock));
570 assert!(preview.is_enabled(PreviewFeature::AddBounds));
571
572 assert_eq!(Preview::from_str(""), Err(EmptyPreviewFeatureNameError));
574 assert!(Preview::from_str("pylock,").is_err());
575 assert!(Preview::from_str(",pylock").is_err());
576
577 let preview = Preview::from_str("unknown-feature,pylock").unwrap();
579 assert!(preview.is_enabled(PreviewFeature::Pylock));
580 assert_eq!(preview.flags.bits().count_ones(), 1);
581 }
582
583 #[test]
584 fn test_preview_display() {
585 let preview = Preview::default();
587 assert_eq!(preview.to_string(), "disabled");
588 let preview = Preview::new(&[]);
589 assert_eq!(preview.to_string(), "disabled");
590
591 let preview = Preview::all();
593 assert_eq!(preview.to_string(), "enabled");
594 assert!(preview.is_enabled(PreviewFeature::TarCodec));
595
596 let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]);
598 assert_eq!(preview.to_string(), "python-install-default");
599
600 let preview = Preview::new(&[PreviewFeature::JsonOutput, PreviewFeature::Pylock]);
602 assert_eq!(preview.to_string(), "json-output,pylock");
603 }
604
605 #[test]
606 fn test_global_preview() {
607 {
608 let _guard =
609 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
610 assert!(!is_enabled(PreviewFeature::InitProjectFlag));
611 assert!(is_enabled(PreviewFeature::Pylock));
612 assert!(is_enabled(PreviewFeature::WorkspaceMetadata));
613 assert!(!is_enabled(PreviewFeature::AuthHelper));
614 }
615 {
616 let _guard =
617 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
618 assert!(is_enabled(PreviewFeature::InitProjectFlag));
619 assert!(!is_enabled(PreviewFeature::Pylock));
620 assert!(!is_enabled(PreviewFeature::WorkspaceMetadata));
621 assert!(is_enabled(PreviewFeature::AuthHelper));
622 }
623 }
624
625 #[test]
626 #[should_panic(
627 expected = "Additional calls to `uv_preview::test::with_features` are not allowed while holding a `FeaturesGuard`"
628 )]
629 fn test_global_preview_panic_nested() {
630 let _guard =
631 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
632 let _guard2 =
633 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
634 }
635
636 #[test]
637 #[should_panic(expected = "uv_preview::test::with_features")]
638 fn test_global_preview_panic_uninitialized() {
639 let _preview = get();
640 }
641}