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