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