uv_preview/lib.rs
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
15/// Indicates if the preview state has been finalized yet or not.
16enum PreviewState {
17 Provisional(Preview),
18 Final(Preview),
19}
20
21/// Indicates how the preview was initialised, to distinguish between normal
22/// code and unit tests.
23enum PreviewMode {
24 /// Initialised by a call to [`init`].
25 Normal(Mutex<PreviewState>),
26 /// Initialised by a call to [`test::with_features`].
27 #[cfg(feature = "testing")]
28 Test(std::sync::RwLock<Option<Preview>>),
29}
30
31static PREVIEW: OnceLock<PreviewMode> = OnceLock::new();
32
33/// Error type for global preview state initialization related errors
34#[derive(Debug, Error)]
35pub enum PreviewError {
36 /// Returned when [`set`] or [`finalize`] are called on a finalized state.
37 #[error("The preview configuration has already been finalized")]
38 AlreadyFinalized,
39
40 /// Returned when [`finalize`] is called on an uninitialized state.
41 #[error("The preview configuration has not been initialized yet")]
42 NotInitialized,
43
44 /// Returned when [`set`] or [`finalize`] are called on a test state.
45 #[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
50/// Initialize the global preview configuration.
51///
52/// This should be called once at startup with the resolved preview settings.
53pub 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 // Calling `set` in a test context is already disallowed, so a panic if
60 // the mutex is poisoned is fine.
61 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 // Calling `set` in a test context is already disallowed, so a panic if
79 // the mutex is poisoned is fine.
80 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
94/// Get the current global preview configuration.
95///
96/// # Panics
97///
98/// When called before [`init`] or (with the `testing` feature) when the
99/// current thread does not hold a [`test::with_features`] guard.
100fn 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 // The unwrap may panic only if the current thread had panicked
114 // while attempting to write the value and then recovered with
115 // `catch_unwind`. This seems unlikely.
116 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
132/// Check if a specific preview feature is enabled globally.
133pub fn is_enabled(flag: PreviewFeature) -> bool {
134 get().is_enabled(flag)
135}
136
137/// Functions for unit tests, do not use from normal code!
138#[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 /// The global preview state test mutex. It does not guard any data but is
145 /// simply used to ensure tests which rely on the global preview state are
146 /// ran serially.
147 static MUTEX: Mutex<()> = Mutex::new(());
148
149 thread_local! {
150 /// Whether the current thread holds the global mutex.
151 ///
152 /// This is used to catch situations where a test forgets to set the
153 /// global test state but happens to work anyway because of another test
154 /// setting the state.
155 pub(crate) static HELD: Cell<bool> = const { Cell::new(false) };
156 }
157
158 /// A scope guard which ensures that the global preview state is configured
159 /// and consistent for the duration of its lifetime.
160 #[derive(Debug)]
161 #[expect(unused)]
162 pub struct FeaturesGuard(MutexGuard<'static, ()>);
163
164 /// Temporarily set the state of preview features for the duration of the
165 /// lifetime of the returned guard.
166 ///
167 /// Calls cannot be nested, and this function must be used to set the global
168 /// preview features when testing functionality which uses it, otherwise
169 /// that functionality will panic.
170 ///
171 /// The preview state will only be valid for the thread which calls this
172 /// function, it will not be valid for any other thread. This is a
173 /// consequence of how `HELD` is used to check for tests which are missing
174 /// the guard.
175 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 // This is okay because the mutex isn't guarding any data, so when
185 // it gets poisoned, it just means a test thread died while holding
186 // it, so it's safe to just re-grab it from the PoisonError, there's
187 // no chance of any corruption.
188 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 /// The `uv python install --default` option installs `python` and `python3` executables
233 /// alongside the versioned executable, making a uv-managed Python available without specifying
234 /// its minor version. When no version is requested and no `.python-version` file is found,
235 /// enabling this feature also makes `uv python install` create these executables by default.
236 /// See [installing Python executables](./python-versions.md#installing-python-executables) for
237 /// details about the installation location and handling of existing executables.
238 PythonInstallDefault,
239 /// Commands that support `--output-format json`, such as `uv tree`, can produce
240 /// machine-readable output for use by other tools. The JSON schemas are experimental and may
241 /// change without warning; enabling this feature silences the preview warning.
242 JsonOutput,
243 /// The `uv pip install` and `uv pip sync` commands can install dependencies from `pylock.toml`
244 /// files, the standardized Python lockfile format. For example, use
245 /// `uv pip install -r pylock.toml` or `uv pip sync pylock.toml`; enabling this feature silences
246 /// the preview warning.
247 Pylock,
248 /// The [`add-bounds`](../reference/settings.md#add-bounds) setting controls the default version
249 /// bounds added by `uv add`. Instead of the default lower bound, dependencies can be
250 /// constrained to a major or minor version range, or pinned to an exact version.
251 AddBounds,
252 /// Workspace members can declare [conflicting
253 /// dependencies](./resolution.md#conflicting-dependencies) at the package level, in addition to
254 /// conflicts between extras or dependency groups. This allows members with incompatible
255 /// requirements to share a lockfile, provided they are not installed together.
256 PackageConflicts,
257 /// The [`extra-build-dependencies`](./projects/config.md#augmenting-build-dependencies) setting
258 /// augments a package's declared build dependencies without disabling build isolation. It can
259 /// supply a missing build dependency or, with `match-runtime = true`, ensure that a build
260 /// dependency uses the same version as the project environment.
261 ExtraBuildDependencies,
262 /// uv warns when multiple packages install conflicting Python modules into the same
263 /// environment. These conflicts can cause imports to depend on installation order, even when
264 /// the packages have different distribution names.
265 DetectModuleConflicts,
266 /// The `uv format` command formats Python code with Ruff, downloading the formatter when
267 /// needed. It can also check formatting with `--check` or show proposed changes with `--diff`;
268 /// enabling this feature silences the preview warning.
269 #[preview(alias = "format")]
270 FormatCommand,
271 /// The `uv auth` commands store credentials in a [system-native
272 /// location](./authentication/http.md#the-uv-credentials-store), using Keychain Services on
273 /// macOS, Credential Manager on Windows, or the Secret Service API on Linux. uv only retrieves
274 /// credentials that it has stored itself, rather than credentials saved by other applications.
275 NativeAuth,
276 /// uv can authenticate requests to an S3-compatible storage endpoint using AWS Signature
277 /// Version 4. Set [`UV_S3_ENDPOINT_URL`](../reference/environment.md#uv_s3_endpoint_url) to
278 /// identify the endpoint; credentials are obtained from the configured AWS credential sources.
279 S3Endpoint,
280 /// The `uv cache size` command reports the total size of uv's cache. It supports human-readable
281 /// output and a machine-readable byte count; enabling this feature silences the preview
282 /// warning.
283 CacheSize,
284 /// Cache cleanup reports the physical disk space reclaimed, accounting for hardlinks and
285 /// copy-on-write clones on macOS and Linux. If an entry's allocated size cannot be measured, uv
286 /// reports a lower bound; other platforms continue to use a coarser estimate. See [clearing the
287 /// cache](./cache.md#clearing-the-cache) for details.
288 CachePhysicalSpace,
289 /// The `uv init` command rejects the deprecated `--project` option. To choose where to create a
290 /// project, pass the target directory as a positional argument, such as `uv init my-project`.
291 InitProjectFlag,
292 /// The `uv workspace metadata` command exposes structured information about a workspace and its
293 /// resolved dependencies for use by other tools. Its output is experimental and may change; use
294 /// `--sync` to include a mapping from importable modules to the packages that provide them.
295 WorkspaceMetadata,
296 /// The `uv workspace dir` command prints the path to the workspace root. Use `--package` to
297 /// print the path to a specific workspace member, for example, with `--package my-package`.
298 WorkspaceDir,
299 /// The `uv workspace list` command lists the names of workspace members, with one name per
300 /// line. Use `--paths` to display their paths instead, for example, when passing workspace
301 /// directories to another tool.
302 WorkspaceList,
303 /// The `uv export --format cyclonedx1.5` command exports a software bill of materials in
304 /// CycloneDX 1.5 JSON format. This describes the locked dependencies in a format that can be
305 /// consumed by software inventory and security tools.
306 SbomExport,
307 /// The [`uv auth helper`](./authentication/cli.md#using-credentials-with-external-tools)
308 /// command lets external tools retrieve HTTP credentials through uv. It currently supports the
309 /// Bazel credential helper protocol, reading a JSON request from standard input and writing a
310 /// JSON response containing authentication headers when credentials are available.
311 AuthHelper,
312 /// For a local `uv run` target, uv starts project and workspace discovery from the directory
313 /// containing the target instead of the current working directory. This feature takes effect
314 /// before configuration is loaded, so it must be enabled on the command line or through an
315 /// environment variable.
316 TargetWorkspaceDiscovery,
317 /// The uv build backend includes `METADATA.json` and `WHEEL.json` files in built wheels
318 /// alongside the standard `METADATA` and `WHEEL` files. These additional files expose package
319 /// and wheel metadata in JSON format.
320 MetadataJson,
321 /// uv can authenticate requests to a Google Cloud Storage endpoint using Google Cloud
322 /// credentials. Set [`UV_GCS_ENDPOINT_URL`](../reference/environment.md#uv_gcs_endpoint_url) to
323 /// identify the endpoint; authentication uses `GOOGLE_APPLICATION_CREDENTIALS` or Application
324 /// Default Credentials.
325 GcsEndpoint,
326 /// On Unix, uv raises the process's soft open-file limit at startup, up to the hard limit. This
327 /// helps avoid "too many open files" errors during concurrent operations, and child processes
328 /// inherit the raised limit.
329 AdjustUlimit,
330 /// Conda environments named `base` or `root` are classified using their paths, like other named
331 /// environments. The name alone does not cause uv to treat a user-created environment as the
332 /// base Conda installation.
333 SpecialCondaEnvNames,
334 /// uv creates relocatable virtual environments by default, using relative paths in their entry
335 /// point and activation scripts. This allows the environment to be moved without invalidating
336 /// those scripts, although arbitrary binaries and nonstandard scripts are not guaranteed to be
337 /// relocatable. Use `uv venv --no-relocatable` to opt out.
338 RelocatableEnvsDefault,
339 /// The `uv publish` command requires distribution filenames to be normalized. Files with
340 /// non-normalized names are skipped when selecting distributions to upload.
341 PublishRequireNormalized,
342 /// The `uv audit` and `uv tool audit` commands check project and installed-tool dependencies
343 /// for known vulnerabilities. Tool audits use the lockfiles recorded by the
344 /// [`tool-install-locks`](#tool-install-locks) feature; enabling both features silences the
345 /// preview warning for `uv tool audit`.
346 #[preview(alias = "audit")]
347 AuditCommand,
348 /// The `--project` option rejects invalid paths instead of warning and continuing in the
349 /// current directory. Except for `uv init`, the path must already exist as a directory or point
350 /// to a `pyproject.toml` file. This feature takes effect before configuration is loaded, so it
351 /// must be enabled on the command line or through an environment variable.
352 ProjectDirectoryMustExist,
353 /// Each configured package index can set its own
354 /// [`exclude-newer`](./indexes.md#configuring-exclude-newer-for-an-index) cutoff, overriding
355 /// the global cutoff for packages served by that index. Set the value to `false` to disable the
356 /// cutoff for an index; package-specific `exclude-newer-package` values still take precedence.
357 IndexExcludeNewer,
358 /// uv can authenticate requests to an Azure Blob Storage endpoint using Azure credentials. Set
359 /// [`UV_AZURE_ENDPOINT_URL`](../reference/environment.md#uv_azure_endpoint_url) to identify the
360 /// endpoint; authentication uses the default Azure credential chain, including Azure CLI
361 /// credentials and workload identity.
362 AzureEndpoint,
363 /// When building source distributions, the uv build backend rewrites `pyproject.toml` as TOML
364 /// 1.0 for compatibility with older build tools. The original file is included as
365 /// `pyproject.toml.orig` in the source distribution.
366 TomlBackwardsCompatibility,
367 /// The `uv sync` command and other installation commands can check packages for malware using
368 /// [OSV](https://osv.dev) before installing them. This checks for known malicious packages,
369 /// while [`uv audit`](#audit-command) checks dependencies for known vulnerabilities.
370 MalwareCheck,
371 /// The `uv venv --clear` option refuses to clear a directory that does not contain a
372 /// `pyvenv.cfg` file. This helps avoid removing unrelated files when the target is not a
373 /// virtual environment; use `--force` to explicitly allow clearing such a directory.
374 VenvSafeClear,
375 /// The `uv check` command runs Python type checking with ty. It uses the project's environment
376 /// to resolve imports and can synchronize that environment before checking; enabling this
377 /// feature silences the preview warning.
378 #[preview(alias = "check")]
379 CheckCommand,
380 /// The `uv init` command creates a packaged application by default, with a `src/` layout, a
381 /// build system, and a script entry point. This gives new applications an installable package
382 /// structure without requiring `--package`.
383 PackagedInit,
384 /// uv stores default [project virtual
385 /// environments](./projects/layout.md#centralized-project-environments) in its cache and
386 /// attempts to link `.venv` to the cached environment. Switching interpreters selects separate
387 /// cached environments that can be reused later. Explicit environment paths and environments
388 /// selected with `--active` are not centralized.
389 CentralizedProjectEnvs,
390 /// uv stores a `uv.lock` file alongside each installed tool and reuses it for subsequent
391 /// installations and upgrades. The lockfile records the tool's resolved dependencies and also
392 /// provides the dependency information used by `uv tool audit`.
393 ToolInstallLocks,
394 /// The `uv workspace list --scripts` command lists standalone Python scripts with inline
395 /// metadata under the workspace root. It prints script paths relative to the workspace root,
396 /// allowing tools to discover scripts separately from workspace members.
397 WorkspaceListScripts,
398 /// Each configured package index can [require a hash
399 /// algorithm](./indexes.md#requiring-a-hash-algorithm) with the `hash-algorithm` setting. uv
400 /// records that algorithm's hash in the lockfile and fails if a distribution does not advertise
401 /// it, instead of selecting another available algorithm.
402 IndexHashAlgorithm,
403 /// Commands using `--locked` or `--check` reject non-canonical lockfile formatting, even if the
404 /// lockfile can be parsed. This makes formatting part of the lockfile check in addition to
405 /// checking whether the resolution is up to date.
406 LockfileFormatCheck,
407 /// uv combines equivalent dependency declarations when writing lockfiles. This reduces
408 /// duplicated declarations in the recorded requirements, constraints, overrides, exclusions,
409 /// and dependency groups.
410 LockfileNormalization,
411 /// uv omits the `package.metadata` tables from `uv.lock`, except for remote URL and Git
412 /// dependencies. Their metadata is retained so uv can check whether those sources are requested
413 /// or stale without network access.
414 LockWithoutMetadata,
415 /// The `--index` and `--default-index` options accept the names of configured package indexes
416 /// as well as URLs. For example, `--index internal` selects the configured index named
417 /// `internal`, so its URL does not need to be repeated on the command line.
418 IndexByName,
419 /// The `uv pip compile --generate-hashes` command restricts the generated hashes to artifacts
420 /// allowed by the selected binary and build policies. For example, when source builds are
421 /// disabled, hashes for source distributions are omitted.
422 ArtifactHashFiltering,
423 /// uv identifies cached wheel archives by a digest of their contents. Matching archive contents
424 /// can share a cache entry, reducing duplication when the same wheel contents are encountered
425 /// more than once.
426 ContentAddressedCache,
427 /// uv omits `exclude-newer-package` entries from the lockfile when the corresponding packages
428 /// are absent from the resolved dependencies. This keeps cutoffs for unrelated packages out of
429 /// `uv.lock`.
430 MissingExcludeNewerPackageLock,
431 /// uv omits redundant runtime constraints and unused overrides, exclusions, dependency
432 /// metadata, and package-specific upload cutoffs from the lockfile. It records which settings
433 /// were consulted during resolution, including backtracking, so settings that affected
434 /// resolution can be retained even when their packages are absent from the final dependency
435 /// graph.
436 ResolutionInputs,
437 /// The `uv export --batch` option exports multiple dependency selections from a TOML manifest
438 /// containing `[[export]]` entries. Each entry specifies an `output-file` and its own package,
439 /// extra, and dependency group selections; output paths are relative to the manifest.
440 BatchExport,
441 /// Frozen project commands can use `uv.lock` without the workspace's `pyproject.toml` file.
442 /// Discovery requires a lockfile with revision 5 or later, which records the workspace
443 /// information needed to select dependencies without the manifest.
444 FrozenLockfile,
445 /// The [`minimum-libc-version`](./resolution.md#minimum-libc-version) setting specifies the
446 /// oldest glibc or musl versions that must be supported during universal resolution. Use it
447 /// with `required-environments` to require compatible wheels for the selected Linux
448 /// environments without excluding wheels for newer libc versions.
449 MinimumLibcVersion,
450 /// Before a nonisolated `uv build`, uv checks that the selected environment satisfies declared,
451 /// backend-reported, and transitive build requirements. This reports missing or incompatible
452 /// build dependencies before building; use `--skip-dependency-check` to skip the check.
453 BuildDependencyCheck,
454 /// uv enables lazy imports when invoking build backends on CPython 3.15 and later. This can
455 /// reduce the work performed by imports during a build, but can also change import-time side
456 /// effects in third-party build backends.
457 BuildLazyImports,
458 /// The `--require-build-hashes` option requires hashes for every build dependency, including
459 /// transitive dependencies. It applies when build dependencies are downloaded during builds,
460 /// project resolution, and installation. See [project build dependency
461 /// hashes](./projects/build.md#project-build-dependency-hashes) for configuration and exceptions.
462 BuildDependencyHashes,
463 /// Enables experimental support for [wheel variants](https://peps.python.org/pep-0825/).
464 /// Variant wheels are not yet selectable.
465 WheelVariants,
466}
467
468impl Display for PreviewFeature {
469 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
470 write!(f, "{}", self.as_str())
471 }
472}
473
474#[derive(Debug, Error, Clone)]
475#[error("Unknown feature flag")]
476pub struct PreviewFeatureParseError;
477
478impl FromStr for PreviewFeature {
479 type Err = PreviewFeatureParseError;
480
481 fn from_str(s: &str) -> Result<Self, Self::Err> {
482 Self::metadata()
483 .iter()
484 .find(|(feature, _, aliases)| feature.as_str() == s || aliases.contains(&s))
485 .map(|(feature, _, _)| *feature)
486 .ok_or(PreviewFeatureParseError)
487 }
488}
489
490#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
491#[error("preview feature name cannot be empty")]
492pub struct EmptyPreviewFeatureNameError;
493
494/// A user-provided preview feature name, which may refer to an unknown feature.
495#[derive(Debug, Clone)]
496pub enum MaybePreviewFeature {
497 Known(PreviewFeature),
498 Unknown(String),
499}
500
501impl FromStr for MaybePreviewFeature {
502 type Err = EmptyPreviewFeatureNameError;
503
504 fn from_str(s: &str) -> Result<Self, Self::Err> {
505 let s = s.trim();
506 if s.is_empty() {
507 return Err(EmptyPreviewFeatureNameError);
508 }
509
510 Ok(match PreviewFeature::from_str(s) {
511 Ok(feature) => Self::Known(feature),
512 Err(_) => Self::Unknown(s.to_string()),
513 })
514 }
515}
516
517impl<'de> serde::Deserialize<'de> for MaybePreviewFeature {
518 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
519 where
520 D: serde::Deserializer<'de>,
521 {
522 let name: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?;
523 Self::from_str(&name).map_err(serde::de::Error::custom)
524 }
525}
526
527#[cfg(feature = "schemars")]
528impl schemars::JsonSchema for MaybePreviewFeature {
529 fn schema_name() -> Cow<'static, str> {
530 Cow::Borrowed("PreviewFeature")
531 }
532
533 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
534 // Advertise canonical names for editor completions, while accepting any nonempty name to
535 // match the forwards-compatible runtime parsing behavior.
536 let choices: Vec<&str> = BitFlags::<PreviewFeature>::all()
537 .iter()
538 .map(PreviewFeature::as_str)
539 .collect();
540 schemars::json_schema!({
541 "type": "string",
542 "anyOf": [
543 {
544 "enum": choices,
545 },
546 {
547 "pattern": "\\S",
548 },
549 ],
550 })
551 }
552}
553
554#[derive(Clone, Copy, PartialEq, Eq, Default)]
555pub struct Preview {
556 flags: BitFlags<PreviewFeature>,
557}
558
559impl Debug for Preview {
560 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
561 let flags: Vec<_> = self.flags.iter().collect();
562 f.debug_struct("Preview").field("flags", &flags).finish()
563 }
564}
565
566impl Preview {
567 #[cfg(any(test, feature = "testing"))]
568 fn new(flags: &[PreviewFeature]) -> Self {
569 Self {
570 flags: flags.iter().copied().fold(BitFlags::empty(), BitOr::bitor),
571 }
572 }
573
574 pub fn all() -> Self {
575 Self {
576 flags: BitFlags::all(),
577 }
578 }
579
580 /// Check if a single feature is enabled.
581 pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
582 self.flags.contains(flag)
583 }
584
585 /// Check if all preview feature rae enabled.
586 pub fn all_enabled(&self) -> bool {
587 self.flags.is_all()
588 }
589
590 /// Check if any preview feature is enabled.
591 pub fn any_enabled(&self) -> bool {
592 !self.flags.is_empty()
593 }
594
595 /// Resolve preview feature names, warning and ignoring unknown names.
596 pub fn from_feature_names<'a>(
597 feature_names: impl IntoIterator<Item = &'a MaybePreviewFeature>,
598 ) -> Self {
599 let mut flags = BitFlags::empty();
600
601 for feature_name in feature_names {
602 match feature_name {
603 MaybePreviewFeature::Known(feature) => flags |= *feature,
604 MaybePreviewFeature::Unknown(feature_name) => {
605 warn_user_once!("Unknown preview feature: `{feature_name}`");
606 }
607 }
608 }
609
610 Self { flags }
611 }
612}
613
614impl Display for Preview {
615 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
616 if self.flags.is_empty() {
617 write!(f, "disabled")
618 } else if self.flags.is_all() {
619 write!(f, "enabled")
620 } else {
621 write!(
622 f,
623 "{}",
624 itertools::join(self.flags.iter().map(PreviewFeature::as_str), ",")
625 )
626 }
627 }
628}
629
630impl FromStr for Preview {
631 type Err = EmptyPreviewFeatureNameError;
632
633 fn from_str(s: &str) -> Result<Self, Self::Err> {
634 let feature_names = s
635 .split(',')
636 .map(MaybePreviewFeature::from_str)
637 .collect::<Result<Vec<_>, _>>()?;
638
639 Ok(Self::from_feature_names(&feature_names))
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646
647 #[test]
648 fn test_preview_feature_from_str() {
649 for &(feature, _, aliases) in PreviewFeature::metadata() {
650 assert_eq!(PreviewFeature::from_str(feature.as_str()).unwrap(), feature);
651
652 for &alias in aliases {
653 assert_eq!(PreviewFeature::from_str(alias).unwrap(), feature);
654 }
655 }
656 }
657
658 #[test]
659 fn test_preview_from_str() {
660 // Test single feature
661 let preview = Preview::from_str("python-install-default").unwrap();
662 assert_eq!(preview.flags, PreviewFeature::PythonInstallDefault);
663
664 // Test multiple features
665 let preview = Preview::from_str("json-output,pylock").unwrap();
666 assert!(preview.is_enabled(PreviewFeature::JsonOutput));
667 assert!(preview.is_enabled(PreviewFeature::Pylock));
668 assert_eq!(preview.flags.bits().count_ones(), 2);
669
670 let preview = Preview::from_str("tool-install-locks").unwrap();
671 assert!(preview.is_enabled(PreviewFeature::ToolInstallLocks));
672
673 // Test with whitespace
674 let preview = Preview::from_str("pylock , add-bounds").unwrap();
675 assert!(preview.is_enabled(PreviewFeature::Pylock));
676 assert!(preview.is_enabled(PreviewFeature::AddBounds));
677
678 // Test empty string error
679 assert_eq!(Preview::from_str(""), Err(EmptyPreviewFeatureNameError));
680 assert!(Preview::from_str("pylock,").is_err());
681 assert!(Preview::from_str(",pylock").is_err());
682
683 // Test unknown feature (should be ignored with warning)
684 let preview = Preview::from_str("unknown-feature,pylock").unwrap();
685 assert!(preview.is_enabled(PreviewFeature::Pylock));
686 assert_eq!(preview.flags.bits().count_ones(), 1);
687 }
688
689 #[test]
690 fn test_preview_display() {
691 // Test disabled
692 let preview = Preview::default();
693 assert_eq!(preview.to_string(), "disabled");
694 let preview = Preview::new(&[]);
695 assert_eq!(preview.to_string(), "disabled");
696
697 // Test enabled (all features)
698 let preview = Preview::all();
699 assert_eq!(preview.to_string(), "enabled");
700
701 // Test single feature
702 let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]);
703 assert_eq!(preview.to_string(), "python-install-default");
704
705 // Test multiple features
706 let preview = Preview::new(&[PreviewFeature::JsonOutput, PreviewFeature::Pylock]);
707 assert_eq!(preview.to_string(), "json-output,pylock");
708 }
709
710 #[test]
711 fn test_global_preview() {
712 {
713 let _guard =
714 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
715 assert!(!is_enabled(PreviewFeature::InitProjectFlag));
716 assert!(is_enabled(PreviewFeature::Pylock));
717 assert!(is_enabled(PreviewFeature::WorkspaceMetadata));
718 assert!(!is_enabled(PreviewFeature::AuthHelper));
719 }
720 {
721 let _guard =
722 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
723 assert!(is_enabled(PreviewFeature::InitProjectFlag));
724 assert!(!is_enabled(PreviewFeature::Pylock));
725 assert!(!is_enabled(PreviewFeature::WorkspaceMetadata));
726 assert!(is_enabled(PreviewFeature::AuthHelper));
727 }
728 }
729
730 #[test]
731 #[should_panic(
732 expected = "Additional calls to `uv_preview::test::with_features` are not allowed while holding a `FeaturesGuard`"
733 )]
734 fn test_global_preview_panic_nested() {
735 let _guard =
736 test::with_features(&[PreviewFeature::Pylock, PreviewFeature::WorkspaceMetadata]);
737 let _guard2 =
738 test::with_features(&[PreviewFeature::InitProjectFlag, PreviewFeature::AuthHelper]);
739 }
740
741 #[test]
742 #[should_panic(expected = "uv_preview::test::with_features")]
743 fn test_global_preview_panic_uninitialized() {
744 let _preview = get();
745 }
746}