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