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