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