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