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