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