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