diff --git a/Cargo.lock b/Cargo.lock index 055dad421a..a5686888e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6839,6 +6839,8 @@ version = "0.0.54" dependencies = [ "enumflags2", "itertools 0.14.0", + "schemars", + "serde", "thiserror", "uv-preview", "uv-warnings", @@ -7162,6 +7164,7 @@ dependencies = [ "fs-err", "schemars", "serde", + "serde-untagged", "textwrap", "thiserror", "toml", @@ -7180,6 +7183,7 @@ dependencies = [ "uv-options-metadata", "uv-pep440", "uv-pep508", + "uv-preview", "uv-pypi-types", "uv-python", "uv-redacted", diff --git a/crates/uv-preview/Cargo.toml b/crates/uv-preview/Cargo.toml index 7e541c824e..132de6b6cb 100644 --- a/crates/uv-preview/Cargo.toml +++ b/crates/uv-preview/Cargo.toml @@ -23,6 +23,8 @@ uv-warnings = { workspace = true } enumflags2 = { workspace = true } itertools = { workspace = true } +schemars = { workspace = true, optional = true } +serde = { workspace = true } thiserror = { workspace = true } [dev-dependencies] @@ -30,4 +32,5 @@ uv-preview = { path = ".", features = ["testing"] } [features] default = [] +schemars = ["dep:schemars"] testing = [] diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 0ebfbffa05..ad9fe9bb09 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::sync::{Mutex, OnceLock}; use std::{ fmt::{Debug, Display, Formatter}, @@ -384,6 +385,43 @@ impl FromStr for MaybePreviewFeature { } } +impl<'de> serde::Deserialize<'de> for MaybePreviewFeature { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let name: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?; + Self::from_str(&name).map_err(serde::de::Error::custom) + } +} + +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for MaybePreviewFeature { + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("PreviewFeature") + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + // Advertise canonical names for editor completions, while accepting any nonempty name to + // match the forwards-compatible runtime parsing behavior. + let choices: Vec<&str> = BitFlags::::all() + .iter() + .map(PreviewFeature::as_str) + .collect(); + schemars::json_schema!({ + "type": "string", + "anyOf": [ + { + "enum": choices, + }, + { + "pattern": "\\S", + }, + ], + }) + } +} + #[derive(Clone, Copy, PartialEq, Eq, Default)] pub struct Preview { flags: BitFlags, @@ -409,23 +447,6 @@ impl Preview { } } - /// Resolve preview arguments, warning and ignoring unknown feature names. - pub fn from_args( - preview: bool, - no_preview: bool, - preview_features: &[MaybePreviewFeature], - ) -> Self { - if no_preview { - return Self::default(); - } - - if preview { - return Self::all(); - } - - Self::from_feature_names(preview_features) - } - /// Check if a single feature is enabled. pub fn is_enabled(&self, flag: PreviewFeature) -> bool { self.flags.contains(flag) @@ -441,7 +462,8 @@ impl Preview { !self.flags.is_empty() } - fn from_feature_names<'a>( + /// Resolve preview feature names, warning and ignoring unknown names. + pub fn from_feature_names<'a>( feature_names: impl IntoIterator, ) -> Self { let mut flags = BitFlags::empty(); @@ -547,34 +569,6 @@ mod tests { assert_eq!(preview.to_string(), "python-upgrade,pylock"); } - #[test] - fn test_preview_from_args() { - // Test no preview and no no_preview, and no features - let preview = Preview::from_args(false, false, &[]); - assert_eq!(preview.to_string(), "disabled"); - - // Test no_preview - let preview = Preview::from_args(true, true, &[]); - assert_eq!(preview.to_string(), "disabled"); - - // Test preview (all features) - let preview = Preview::from_args(true, false, &[]); - assert_eq!(preview.to_string(), "enabled"); - - // Test specific features - let features = ["python-upgrade", "json-output"].map(|name| name.parse().unwrap()); - let preview = Preview::from_args(false, false, &features); - assert!(preview.is_enabled(PreviewFeature::PythonUpgrade)); - assert!(preview.is_enabled(PreviewFeature::JsonOutput)); - assert!(!preview.is_enabled(PreviewFeature::Pylock)); - - // Test unknown features - let features = ["unknown-feature", "pylock"].map(|name| name.parse().unwrap()); - let preview = Preview::from_args(false, false, &features); - assert!(preview.is_enabled(PreviewFeature::Pylock)); - assert_eq!(preview.flags.bits().count_ones(), 1); - } - #[test] fn test_preview_feature_as_str() { assert_eq!( diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index 2387c86752..2132edb030 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -30,6 +30,7 @@ uv-normalize = { workspace = true } uv-options-metadata = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } +uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true, features = ["clap"] } uv-redacted = { workspace = true } @@ -44,6 +45,7 @@ clap = { workspace = true } fs-err = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true } +serde-untagged = { workspace = true } textwrap = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } @@ -51,7 +53,7 @@ tracing = { workspace = true } url = { workspace = true } [package.metadata.cargo-shear] -ignored = ["uv-options-metadata", "clap"] +ignored = ["clap"] [features] schemars = [ @@ -61,6 +63,7 @@ schemars = [ "uv-distribution-types/schemars", "uv-install-wheel/schemars", "uv-normalize/schemars", + "uv-preview/schemars", "uv-python/schemars", "uv-resolver/schemars", "uv-torch/schemars", diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 256d82a83d..7ea888e0da 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -23,7 +23,7 @@ use uv_torch::TorchMode; use uv_workspace::pyproject::ExtraBuildDependencies; use uv_workspace::pyproject_mut::AddBoundsKind; -use crate::{AuditOptions, FilesystemOptions, Options, PipOptions}; +use crate::{AuditOptions, FilesystemOptions, Options, PipOptions, PreviewOption}; pub trait Combine { /// Combine two values, preferring the values in `self`. @@ -109,6 +109,7 @@ impl_combine_or!(PipExtraIndex); impl_combine_or!(PipFindLinks); impl_combine_or!(PipIndex); impl_combine_or!(PrereleaseMode); +impl_combine_or!(PreviewOption); impl_combine_or!(ProxyUrl); impl_combine_or!(PythonDownloads); impl_combine_or!(PythonPreference); diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index f66abcc3b7..cf00f8b2a2 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -481,8 +481,10 @@ fn warn_uv_toml_masked_fields(options: &Options) { if cache_dir.is_some() { masked_fields.push("cache-dir"); } - if preview.is_some() { - masked_fields.push("preview"); + match preview { + Some(PreviewOption::Preview(_)) => masked_fields.push("preview"), + Some(PreviewOption::PreviewFeatures(_)) => masked_fields.push("preview-features"), + None => (), } if python_preference.is_some() { masked_fields.push("python-preference"); diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index dc5f6ecc24..50583bc527 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "schemars")] +use std::borrow::Cow; use std::{fmt::Debug, num::NonZeroUsize, path::Path, path::PathBuf}; use serde::{Deserialize, Serialize}; @@ -15,6 +17,7 @@ use uv_install_wheel::LinkMode; use uv_macros::{CombineOptions, OptionsMetadata}; use uv_normalize::{ExtraName, PackageName, PipGroupName}; use uv_pep508::Requirement; +use uv_preview::{MaybePreviewFeature, Preview}; use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_redacted::DisplaySafeUrl; @@ -69,9 +72,9 @@ pub(crate) struct UvRequiredVersionToml { /// A `[tool.uv]` section. #[allow(dead_code)] #[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)] -#[serde(from = "OptionsWire", rename_all = "kebab-case")] +#[serde(try_from = "OptionsWire", rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[cfg_attr(feature = "schemars", schemars(!from))] +#[cfg_attr(feature = "schemars", schemars(!try_from))] pub struct Options { #[serde(flatten)] pub globals: GlobalOptions, @@ -246,8 +249,9 @@ impl Options { /// Global settings, relevant to all invocations. #[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)] -#[serde(rename_all = "kebab-case")] +#[serde(try_from = "GlobalOptionsWire", rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "schemars", schemars(!try_from))] pub struct GlobalOptions { /// Enforce a requirement on the version of uv. /// @@ -324,15 +328,11 @@ pub struct GlobalOptions { "# )] pub cache_dir: Option, - /// Whether to enable experimental, preview features. - #[option( - default = "false", - value_type = "bool", - example = r#" - preview = true - "# - )] - pub preview: Option, + + /// The user's preview configuration. + #[serde(flatten)] + pub preview: Option, + /// Whether to prefer using Python installations that are already present on the system, or /// those that are downloaded and installed by uv. #[option( @@ -435,6 +435,78 @@ pub struct GlobalOptions { pub allow_insecure_host: Option>, } +/// Like [`GlobalOptions`], but with any `#[serde(flatten)]` fields inlined. +/// This improves line/column information in error messages. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct GlobalOptionsWire { + required_version: Option, + system_certs: Option, + native_tls: Option, + offline: Option, + no_cache: Option, + cache_dir: Option, + + preview: Option, + preview_features: Option, + + python_preference: Option, + python_downloads: Option, + concurrent_downloads: Option, + concurrent_builds: Option, + concurrent_installs: Option, + http_proxy: Option, + https_proxy: Option, + no_proxy: Option>, + allow_insecure_host: Option>, +} + +impl TryFrom for GlobalOptions { + type Error = &'static str; + + #[allow(deprecated)] + fn try_from(value: GlobalOptionsWire) -> Result { + let GlobalOptionsWire { + required_version, + system_certs, + native_tls, + offline, + no_cache, + cache_dir, + preview, + preview_features, + python_preference, + python_downloads, + concurrent_downloads, + concurrent_builds, + concurrent_installs, + http_proxy, + https_proxy, + no_proxy, + allow_insecure_host, + } = value; + + Ok(Self { + required_version, + system_certs, + native_tls, + offline, + no_cache, + cache_dir, + preview: PreviewOption::try_from(preview, preview_features)?, + python_preference, + python_downloads, + concurrent_downloads, + concurrent_builds, + concurrent_installs, + http_proxy, + https_proxy, + no_proxy, + allow_insecure_host, + }) + } +} + /// Settings relevant to all installer operations. #[derive(Debug, Clone, Default, CombineOptions)] pub struct InstallerOptions { @@ -2381,6 +2453,7 @@ struct OptionsWire { no_cache: Option, cache_dir: Option, preview: Option, + preview_features: Option, python_preference: Option, python_downloads: Option, concurrent_downloads: Option, @@ -2472,9 +2545,11 @@ struct OptionsWire { build_backend: Option, } -impl From for Options { +impl TryFrom for Options { + type Error = &'static str; + #[allow(deprecated)] - fn from(value: OptionsWire) -> Self { + fn try_from(value: OptionsWire) -> Result { let OptionsWire { required_version, system_certs, @@ -2483,6 +2558,7 @@ impl From for Options { no_cache, cache_dir, preview, + preview_features, python_preference, python_downloads, python_install_mirror, @@ -2552,7 +2628,7 @@ impl From for Options { build_backend, } = value; - Self { + Ok(Self { globals: GlobalOptions { required_version, system_certs, @@ -2560,7 +2636,7 @@ impl From for Options { offline, no_cache, cache_dir, - preview, + preview: PreviewOption::try_from(preview, preview_features)?, python_preference, python_downloads, concurrent_downloads, @@ -2635,7 +2711,7 @@ impl From for Options { dependency_groups, managed, package, - } + }) } } @@ -2765,3 +2841,116 @@ impl From<&crate::EnvironmentOptions> for MalwareCheckSettings { } } } + +/// Represents the `preview-features` configuration option. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "schemars", schemars(untagged))] +pub enum PreviewFeaturesOption { + Toggle(bool), + Features(Vec), +} + +// A derived `#[serde(untagged)]` implementation collapses detailed type and element errors into +// "data did not match any variant", so use a type-directed visitor to preserve useful diagnostics. +impl<'de> Deserialize<'de> for PreviewFeaturesOption { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + serde_untagged::UntaggedEnumVisitor::new() + .expecting("a boolean or a list of preview feature names") + .bool(|value| Ok(Self::Toggle(value))) + .seq(|sequence| sequence.deserialize().map(Self::Features)) + .deserialize(deserializer) + } +} + +#[expect( + dead_code, + reason = "Fields are only used by the OptionsMetadata and JsonSchema derives" +)] +#[derive(OptionsMetadata)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "schemars", schemars(rename_all = "kebab-case"))] +struct PreviewOptionsDefinition { + // This legacy setting remains supported and included in the JSON schema, but is omitted from + // option metadata so the generated settings reference documents only `preview-features`. + /// Whether to enable all experimental, preview features. + /// + /// Use `preview-features` instead. + #[deprecated(note = "use `preview-features` instead")] + preview: Option, + /// Whether to enable specific or all experimental preview features. + /// + /// Unknown feature names are ignored with a warning. + #[option( + default = "false", + value_type = "bool | list[str]", + example = r#" + preview-features = true + # or + preview-features = ["python-upgrade"] + "# + )] + preview_features: Option, +} + +/// Represents the user's preview configuration from either `preview` or `preview-features`. +#[derive(Debug, Clone)] +pub enum PreviewOption { + /// Whether to enable all experimental, preview features. + Preview(bool), + /// Whether to enable specific or all experimental preview features. + PreviewFeatures(PreviewFeaturesOption), +} + +impl uv_options_metadata::OptionsMetadata for PreviewOption { + fn record(visit: &mut dyn uv_options_metadata::Visit) { + ::record(visit); + } +} + +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for PreviewOption { + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("PreviewOption") + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + let mut schema = ::json_schema(generator); + schema.insert( + "not".to_string(), + schemars::json_schema!({ + "required": ["preview", "preview-features"], + }) + .into(), + ); + schema + } +} + +impl PreviewOption { + fn try_from( + preview: Option, + preview_features: Option, + ) -> Result, &'static str> { + match (preview, preview_features) { + (Some(_), Some(_)) => Err("cannot specify both `preview` and `preview-features`"), + (Some(b), None) => Ok(Some(Self::Preview(b))), + (None, Some(features)) => Ok(Some(Self::PreviewFeatures(features))), + (None, None) => Ok(None), + } + } + + /// Resolve the preview configuration, warning and ignoring unknown feature names. + pub fn resolve(&self) -> Preview { + use PreviewFeaturesOption::{Features, Toggle}; + + match self { + Self::Preview(false) | Self::PreviewFeatures(Toggle(false)) => Preview::default(), + Self::Preview(true) | Self::PreviewFeatures(Toggle(true)) => Preview::all(), + Self::PreviewFeatures(Features(features)) => Preview::from_feature_names(features), + } + } +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 9e98111c2b..482cf6fed4 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -39,7 +39,7 @@ use uv_fs::{CWD, Simplified, normalize_path}; #[cfg(feature = "self-update")] use uv_pep440::release_specifiers_to_ranges; use uv_pep508::VersionOrUrl; -use uv_preview::{Preview, PreviewFeature}; +use uv_preview::PreviewFeature; use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl}; use uv_python::PythonRequest; use uv_requirements::{GroupsSpecification, RequirementsSource}; @@ -127,11 +127,7 @@ async fn run(cli: Cli) -> Result { let environment = EnvironmentOptions::new()?; // Resolve preview flags before config discovery for decisions that affect the discovery root. - let early_preview = Preview::from_args( - settings::resolve_preview(&cli.top_level.global_args, None, &environment), - cli.top_level.global_args.no_preview, - &cli.top_level.global_args.preview_features, - ); + let early_preview = settings::resolve_preview(&cli.top_level.global_args, None, &environment); // Make the early preview flags globally available. uv_preview::set(early_preview)?; diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 574b18c9ef..2323d7d72b 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -57,8 +57,8 @@ use uv_resolver::{ }; use uv_settings::{ Combine, EnvironmentOptions, FilesystemOptions, MalwareCheckSettings, Options, PipOptions, - PublishOptions, PythonInstallMirrors, ResolverInstallerOptions, ResolverInstallerSchema, - ResolverOptions, + PreviewFeaturesOption, PreviewOption, PublishOptions, PythonInstallMirrors, + ResolverInstallerOptions, ResolverInstallerSchema, ResolverOptions, }; use uv_static::EnvVars; use uv_torch::{AmdGpuArchitecture, TorchMode}; @@ -129,11 +129,7 @@ impl GlobalSettings { .unwrap_or_else(Concurrency::threads), ), show_settings: args.show_settings, - preview: Preview::from_args( - resolve_preview(args, workspace, environment), - args.no_preview, - &args.preview_features, - ), + preview: resolve_preview(args, workspace, environment), python_preference, python_downloads: flag( args.allow_python_downloads, @@ -228,22 +224,41 @@ pub(crate) fn resolve_preview( args: &GlobalArgs, workspace: Option<&FilesystemOptions>, environment: &EnvironmentOptions, -) -> bool { - // CLI takes precedence - match flag(args.preview, args.no_preview, "preview") { - Some(value) => value, - None => { - // Check environment variable - if environment.preview.value == Some(true) { - true - } else { - // Fall back to workspace config - workspace - .and_then(|workspace| workspace.globals.preview) - .unwrap_or(false) - } - } +) -> Preview { + // Explicit `--preview` and `--no-preview` flags take priority. + if let Some(enabled) = flag(args.preview, args.no_preview, "preview") { + return if enabled { + Preview::all() + } else { + Preview::default() + }; } + + // `UV_PREVIEW=true` enables all preview features. + if environment.preview.value == Some(true) { + return Preview::all(); + } + + let configured = workspace.and_then(|workspace| workspace.globals.preview.as_ref()); + + // Boolean enable-all configuration takes priority. + if matches!( + configured, + Some( + PreviewOption::Preview(true) + | PreviewOption::PreviewFeatures(PreviewFeaturesOption::Toggle(true)) + ) + ) { + return Preview::all(); + } + + // Explicit preview feature names take priority over configured feature names. + if !args.preview_features.is_empty() { + return Preview::from_feature_names(&args.preview_features); + } + + // Fall back to workspace configuration. + configured.map(PreviewOption::resolve).unwrap_or_default() } /// The resolved network settings to use for any invocation of the CLI. diff --git a/crates/uv/tests/pip_install/pip_install.rs b/crates/uv/tests/pip_install/pip_install.rs index 8148d4bad9..94bf93d8d8 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -265,7 +265,7 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { | 2 | unknown = "field" | ^^^^^^^ - unknown field `unknown`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` + unknown field `unknown`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `preview-features`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` Resolved in [TIME] Checked in [TIME] diff --git a/crates/uv/tests/sync/show_settings.rs b/crates/uv/tests/sync/show_settings.rs index 6ff0e0c0dc..8d05984e48 100644 --- a/crates/uv/tests/sync/show_settings.rs +++ b/crates/uv/tests/sync/show_settings.rs @@ -2119,6 +2119,95 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { Ok(()) } +/// Read preview settings from both a `uv.toml` and `pyproject.toml` file in the current directory. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_both_preview() -> anyhow::Result<()> { + let context = uv_test::test_context!("3.12"); + + let uv_config = context.temp_dir.child("uv.toml"); + let pyproject = context.temp_dir.child("pyproject.toml"); + + let baseline = capture_uv_snapshot!( + context.filters(), + add_shared_args(context.version()).arg("--show-settings") + ); + + pyproject.write_str(indoc::indoc! {r" + [tool.uv] + preview = true + "})?; + uv_config.write_str(r#"preview-features = ["pylock"]"#)?; + + // The warning should name the ignored `preview` setting. + // Compare against the baseline. + let preview_masked = diff_uv_snapshot!( + context.filters(), + &baseline, + add_shared_args(context.version()).arg("--show-settings"), + @" + ... + }, + show_settings: true, + preview: Preview { + - flags: [], + + flags: [ + + Pylock, + + ], + }, + python_preference: Managed, + python_downloads: Automatic, + ... + } + + ----- stderr ----- + +warning: Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`. The following fields from `[tool.uv]` will be ignored in favor of the `uv.toml` file: + +- preview + ... + " + ); + + pyproject.write_str(indoc::indoc! {r#" + [tool.uv] + preview-features = ["unknown-preview-feature"] + "#})?; + uv_config.write_str("preview = false")?; + + // The warning should name the ignored `preview-features` setting without warning about its + // unknown feature name. + // Compare against the inverse spelling combination. + diff_uv_snapshot!( + context.filters(), + &preview_masked, + add_shared_args(context.version()).arg("--show-settings"), + @" + ... + }, + show_settings: true, + preview: Preview { + - flags: [ + - Pylock, + - ], + + flags: [], + }, + python_preference: Managed, + python_downloads: Automatic, + ... + + ----- stderr ----- + warning: Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`. The following fields from `[tool.uv]` will be ignored in favor of the `uv.toml` file: + -- preview + +- preview-features + ... + " + ); + + Ok(()) +} + /// Tests that errors when parsing `conflicts` are reported. #[test] fn invalid_conflicts() -> anyhow::Result<()> { @@ -2404,7 +2493,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `preview-features`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend` " ); @@ -3088,6 +3177,727 @@ fn preview_features() { ); } +/// Test preview precedence across configuration, environment, and CLI settings. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn preview_precedence() -> anyhow::Result<()> { + let context = uv_test::test_context!("3.12"); + + let show_settings = || { + let mut cmd = context.version(); + cmd.arg("--show-settings"); + add_shared_args(cmd) + }; + + let disabled = capture_uv_snapshot!(context.filters(), show_settings()); + + let enabled = capture_uv_snapshot!(context.filters(), show_settings().arg("--preview")); + + let user_config_dir = context.user_config_dir.child("uv"); + user_config_dir.create_dir_all()?; + let user_config = user_config_dir.child("uv.toml"); + user_config.write_str("preview = true")?; + + // `preview = true` in user `uv.toml` should be the same as passing `--preview`. + diff_uv_snapshot!( + context.filters(), + &enabled, + show_settings(), + @"" + ); + + let project_config = context.temp_dir.child("pyproject.toml"); + project_config.write_str(indoc::indoc! {r" + [tool.uv] + preview = false + "})?; + + // `preview = false` in `pyproject.toml` should mask user `uv.toml`. + diff_uv_snapshot!( + context.filters(), + &disabled, + show_settings(), + @"" + ); + + // `--preview-features` should mask `preview = false` in a config file. + diff_uv_snapshot!( + context.filters(), + &disabled, + show_settings().arg("--preview-features").arg("pylock"), + @" + ... + }, + show_settings: true, + preview: Preview { + - flags: [], + + flags: [ + + Pylock, + + ], + }, + python_preference: Managed, + python_downloads: Automatic, + ... + " + ); + + user_config.write_str("preview = false")?; + + project_config.write_str(indoc::indoc! {r" + [tool.uv] + preview = true + "})?; + + // `preview = true` in a config file should mask `--preview-features`. + diff_uv_snapshot!( + context.filters(), + &enabled, + show_settings().arg("--preview-features").arg("pylock"), + @"" + ); + + // `UV_PREVIEW=false` should fall through to configuration. + // Compare against `--preview`. + diff_uv_snapshot!( + context.filters(), + &enabled, + show_settings().env(EnvVars::UV_PREVIEW, "0"), + @"" + ); + + project_config.write_str("")?; + user_config.write_str("")?; + + // `UV_PREVIEW=true` should override any explicit CLI feature list. + diff_uv_snapshot!( + context.filters(), + &enabled, + show_settings() + .arg("--preview-features") + .arg("pylock") + .env(EnvVars::UV_PREVIEW, "1"), + @"" + ); + + // `--no-preview` should override `UV_PREVIEW=true`. + diff_uv_snapshot!( + context.filters(), + &disabled, + show_settings() + .arg("--no-preview") + .env(EnvVars::UV_PREVIEW, "1"), + @"" + ); + + user_config.write_str("preview = true")?; + project_config.write_str(indoc::indoc! {r#" + [tool.uv] + preview-features = ["format-command"] + "#})?; + + // The project setting atomically overrides the differently-spelled user setting. + // Compare against the disabled baseline. + let project = diff_uv_snapshot!( + context.filters(), + &disabled, + show_settings(), + @" + ... + }, + show_settings: true, + preview: Preview { + - flags: [], + + flags: [ + + Format, + + ], + }, + python_preference: Managed, + python_downloads: Automatic, + ... + " + ); + + user_config.write_str(r#"preview-features = ["unknown-preview-feature"]"#)?; + + // An unknown lower-priority setting is masked without warning. + // Compare against the recognized project setting. + diff_uv_snapshot!( + context.filters(), + &project, + show_settings(), + @"" + ); + + user_config.write_str("preview = true")?; + project_config.write_str(indoc::indoc! {r" + [tool.uv] + preview-features = false + "})?; + + // An explicit disabled project setting atomically overrides the enabled user setting. + // Compare against the disabled baseline. + diff_uv_snapshot!( + context.filters(), + &disabled, + show_settings(), + @"" + ); + + project_config.write_str(indoc::indoc! {r#" + [tool.uv] + preview-features = ["format-command"] + "#})?; + + // An explicit CLI feature list should override the project feature list. + // Compare against the project feature list. + let cli_features = diff_uv_snapshot!( + context.filters(), + &project, + show_settings().arg("--preview-features").arg("pylock"), + @" + ... + show_settings: true, + preview: Preview { + flags: [ + - Format, + + Pylock, + ], + }, + python_preference: Managed, + ... + " + ); + + project_config.write_str(indoc::indoc! {r" + [tool.uv] + preview-features = false + "})?; + + // An explicit CLI feature list should override the disabled project setting. + // Compare against the explicit CLI feature list. + diff_uv_snapshot!( + context.filters(), + &cli_features, + show_settings().arg("--preview-features").arg("pylock"), + @"" + ); + + project_config.write_str(indoc::indoc! {r#" + [tool.uv] + preview-features = ["unknown-preview-feature"] + "#})?; + + // An unknown-only project setting atomically overrides the enabled user setting. + // Compare against the disabled baseline. + diff_uv_snapshot!( + context.filters(), + &disabled, + show_settings(), + @" + ... + } + + ----- stderr ----- + +warning: Unknown preview feature: `unknown-preview-feature` + ... + " + ); + + // An explicit CLI setting masks the unknown project setting without warning. + // Compare against the explicit CLI feature list. + diff_uv_snapshot!( + context.filters(), + &cli_features, + show_settings().arg("--preview-features").arg("pylock"), + @"" + ); + + // `--preview` masks the unknown project setting without warning. + // Compare against `--preview`. + diff_uv_snapshot!( + context.filters(), + &enabled, + show_settings().arg("--preview"), + @"" + ); + + project_config.write_str(indoc::indoc! {r" + [tool.uv] + preview-features = true + "})?; + + // Both enable-all spellings have the same precedence. + // Compare against `--preview`. + diff_uv_snapshot!( + context.filters(), + &enabled, + show_settings().arg("--preview-features").arg("pylock"), + @"" + ); + + Ok(()) +} + +/// Test `preview` and `preview-features` parsing in `uv.toml`. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn preview_features_uv_toml() -> anyhow::Result<()> { + let context = uv_test::test_context!("3.12"); + + let config = context.temp_dir.child("uv.toml"); + + let enabled = capture_uv_snapshot!( + context.filters(), + add_shared_args(context.version()) + .arg("--show-settings") + .arg("--preview") + ); + + let disabled = capture_uv_snapshot!( + context.filters(), + add_shared_args(context.version()).arg("--show-settings") + ); + + config.write_str(r#"preview-features = ["format-command"]"#)?; + + // A feature list should enable only the named preview features. + // Compare against the disabled baseline. + diff_uv_snapshot!( + context.filters(), + &disabled, + add_shared_args(context.version()).arg("--show-settings"), + @" + ... + }, + show_settings: true, + preview: Preview { + - flags: [], + + flags: [ + + Format, + + ], + }, + python_preference: Managed, + python_downloads: Automatic, + ... + " + ); + + config.write_str("preview-features = true")?; + + // `preview-features = true` should enable all preview features. + // Compare against `--preview`. + diff_uv_snapshot!( + context.filters(), + &enabled, + add_shared_args(context.version()).arg("--show-settings"), + @"" + ); + + config.write_str("preview-features = false")?; + + // `preview-features = false` should be equivalent to `preview = false`. + // Compare against the disabled baseline. + diff_uv_snapshot!( + context.filters(), + &disabled, + add_shared_args(context.version()).arg("--show-settings"), + @"" + ); + + config.write_str("preview-features = []")?; + + // An empty feature list should not enable any preview features. + // Compare against the disabled baseline. + diff_uv_snapshot!( + context.filters(), + &disabled, + add_shared_args(context.version()).arg("--show-settings"), + @"" + ); + + config.write_str(indoc::indoc! {r#" + preview = true + preview-features = ["format-command"] + "#})?; + + // The two settings should be rejected when used together. + uv_snapshot!(context.filters(), add_shared_args(context.version()).arg("--show-settings"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to parse: `uv.toml` + Caused by: cannot specify both `preview` and `preview-features` + "); + + config.write_str(r#"preview-features = ["unknown-preview-feature"]"#)?; + + // Unknown names should warn without enabling any recognized preview features. + // Compare against the disabled baseline. + diff_uv_snapshot!( + context.filters(), + &disabled, + add_shared_args(context.version()).arg("--show-settings"), + @" + ... + } + + ----- stderr ----- + +warning: Unknown preview feature: `unknown-preview-feature` + ... + " + ); + + config.write_str(r#"preview-features = [" "]"#)?; + + // Empty preview feature names should be rejected. + uv_snapshot!(context.filters(), add_shared_args(context.version()).arg("--show-settings"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to parse: `uv.toml` + Caused by: TOML parse error at line 1, column 20 + | + 1 | preview-features = [\" \"] + | ^^^^^^ + preview feature name cannot be empty + "); + + config.write_str("preview-features = 123")?; + + // Invalid preview feature types should be rejected. + uv_snapshot!(context.filters(), add_shared_args(context.version()).arg("--show-settings"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to parse: `uv.toml` + Caused by: TOML parse error at line 1, column 20 + | + 1 | preview-features = 123 + | ^^^ + invalid type: integer `123`, expected a boolean or a list of preview feature names + "); + + Ok(()) +} + +/// Test preview setting discovery and diagnostics in `pyproject.toml`. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn preview_features_pyproject_toml() -> anyhow::Result<()> { + let context = uv_test::test_context!("3.12"); + + let pyproject = context.temp_dir.child("pyproject.toml"); + + let baseline = capture_uv_snapshot!( + context.filters(), + add_shared_args(context.version()).arg("--show-settings") + ); + + pyproject.write_str(indoc::indoc! {r#" + [tool.uv] + preview-features = ["format-command"] + "#})?; + + // A feature list should enable only the named preview features. + // Compare against the baseline. + diff_uv_snapshot!( + context.filters(), + &baseline, + add_shared_args(context.version()).arg("--show-settings"), + @" + ... + }, + show_settings: true, + preview: Preview { + - flags: [], + + flags: [ + + Format, + + ], + }, + python_preference: Managed, + python_downloads: Automatic, + ... + " + ); + + pyproject.write_str(indoc::indoc! {r#" + [tool.uv] + preview = true + preview-features = ["format-command"] + "#})?; + + // Conflicting preview settings should be ignored during settings discovery with a warning. + // Compare against the baseline. + diff_uv_snapshot!( + context.filters(), + &baseline, + add_shared_args(context.version()).arg("--show-settings"), + @" + ... + } + + ----- stderr ----- + +warning: Failed to parse `pyproject.toml` during settings discovery: + + TOML parse error at line 1, column 1 + + | + + 1 | [tool.uv] + + | ^^^^^^^^^ + + cannot specify both `preview` and `preview-features` + + + ... + " + ); + + pyproject.write_str(indoc::indoc! {r" + [tool.uv] + preview-features = 123 + "})?; + + // Invalid preview feature types should be ignored during settings discovery with a warning. + // Compare against the baseline. + diff_uv_snapshot!( + context.filters(), + &baseline, + add_shared_args(context.version()).arg("--show-settings"), + @" + ... + } + + ----- stderr ----- + +warning: Failed to parse `pyproject.toml` during settings discovery: + + TOML parse error at line 2, column 20 + + | + + 2 | preview-features = 123 + + | ^^^ + + invalid type: integer `123`, expected a boolean or a list of preview feature names + + + ... + " + ); + + Ok(()) +} + +/// Test PEP 723-specific preview parsing, precedence, and diagnostics. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn run_pep723_script_preview_features() -> anyhow::Result<()> { + let context = uv_test::test_context!("3.12"); + + let show_settings = || { + let mut cmd = context.run(); + cmd.arg("--show-settings").arg("main.py"); + cmd + }; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc::indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [] + # /// + + print("hello") + "# + })?; + + let baseline = capture_uv_snapshot!(context.filters(), show_settings()); + + let enabled = capture_uv_snapshot!( + context.filters(), + context + .run() + .arg("--show-settings") + .arg("--preview") + .arg("main.py") + ); + + test_script.write_str(indoc::indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [] + # + # [tool.uv] + # preview-features = ["format-command"] + # /// + + print("hello") + "# + })?; + + let preview_features = diff_uv_snapshot!( + context.filters(), + &baseline, + show_settings(), + @" + ... + }, + show_settings: true, + preview: Preview { + - flags: [], + + flags: [ + + Format, + + ], + }, + python_preference: Managed, + python_downloads: Never, + ... + " + ); + + test_script.write_str(indoc::indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [] + # + # [tool.uv] + # preview-features = ["format-command", "unknown-preview-feature"] + # /// + + print("hello") + "# + })?; + + // Unknown names should warn without changing the recognized feature set. + // Compare against the recognized feature list to isolate the warning. + diff_uv_snapshot!( + context.filters(), + &preview_features, + show_settings(), + @" + ... + } + + ----- stderr ----- + +warning: Unknown preview feature: `unknown-preview-feature` + ... + " + ); + + test_script.write_str(indoc::indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [] + # + # [tool.uv] + # preview = true + # /// + + print("hello") + "# + })?; + + // The legacy `preview = true` setting should enable all preview features. + // Compare against `--preview`. + diff_uv_snapshot!( + context.filters(), + &enabled, + show_settings(), + @"" + ); + + test_script.write_str(indoc::indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [] + # + # [tool.uv] + # preview-features = [123] + # /// + + print("hello") + "# + })?; + + // The diagnostic should reject a non-string preview feature name. + uv_snapshot!(context.filters(), show_settings(), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: TOML parse error at line 4, column 1 + | + 4 | [tool.uv] + | ^^^^^^^^^ + invalid type: integer `123`, expected a string + "); + + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc::indoc! {r" + [tool.uv] + preview = true + "})?; + test_script.write_str(indoc::indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [] + # + # [tool.uv] + # preview-features = false + # /// + + print("hello") + "# + })?; + + // The script setting should atomically override the project setting. + assert_eq!( + baseline, + capture_uv_snapshot!(context.filters(), show_settings()) + ); + + test_script.write_str(indoc::indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [] + # + # [tool.uv] + # preview = true + # preview-features = ["format-command"] + # /// + + print("hello") + "# + })?; + + // The two settings should be rejected when used together. + uv_snapshot!(context.filters(), show_settings(), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: TOML parse error at line 4, column 1 + | + 4 | [tool.uv] + | ^^^^^^^^^ + cannot specify both `preview` and `preview-features` + "); + + Ok(()) +} + #[test] #[cfg_attr( windows, diff --git a/docs/concepts/preview.md b/docs/concepts/preview.md index 363270e4d7..cb4b520949 100644 --- a/docs/concepts/preview.md +++ b/docs/concepts/preview.md @@ -41,7 +41,20 @@ The `UV_PREVIEW_FEATURES` environment variable can be used similarly, e.g.: $ UV_PREVIEW_FEATURES=foo,bar uv run ... ``` -For backwards compatibility, enabling preview features that do not exist will warn, but not error. +Preview features can also be enabled in `uv.toml`, or under `[tool.uv]` in `pyproject.toml` and PEP +723 metadata: + +```toml +preview-features = ["foo", "bar"] +``` + +Set `preview-features = true` to enable all preview features. + +Some preview features take effect before configuration files are loaded and cannot be enabled from +configuration. + +For backwards compatibility, enabling preview features that do not exist will warn, but not error, +regardless of the source. ## Using preview features @@ -71,6 +84,12 @@ The following preview features are available: - `workspace-metadata`: Allows using `uv workspace metadata`. - `workspace-dir`: Allows using `uv workspace dir`. - `workspace-list`: Allows using `uv workspace list`. +- `target-workspace-discovery`: Uses the directory containing a local `uv run` target, rather than + the current working directory, as the starting point for project and workspace discovery. This + feature takes effect before configuration is loaded. +- `project-directory-must-exist`: Rejects an invalid `--project` path instead of warning and + continuing. Except for `uv init`, the path must already exist as a directory or point to a + `pyproject.toml` file. This feature takes effect before configuration is loaded. - `malware-check`: Allows `uv sync` and other commands to check for malware using [OSV](https://osv.dev) before installing packages. diff --git a/uv.schema.json b/uv.schema.json index 17b4079093..1d209e8a40 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -432,8 +432,20 @@ ] }, "preview": { - "description": "Whether to enable experimental, preview features.", - "type": ["boolean", "null"] + "description": "Whether to enable all experimental, preview features.\n\nUse `preview-features` instead.", + "type": ["boolean", "null"], + "deprecated": true + }, + "preview-features": { + "description": "Whether to enable specific or all experimental preview features.\n\nUnknown feature names are ignored with a warning.", + "anyOf": [ + { + "$ref": "#/definitions/PreviewFeaturesOption" + }, + { + "type": "null" + } + ] }, "publish-url": { "description": "The URL for publishing packages to the Python package index (by default:\n).", @@ -581,6 +593,9 @@ } }, "additionalProperties": false, + "not": { + "required": ["preview", "preview-features"] + }, "definitions": { "AddBoundsKind": { "description": "The default version specifier when adding a dependency.", @@ -1619,6 +1634,67 @@ } ] }, + "PreviewFeature": { + "type": "string", + "anyOf": [ + { + "enum": [ + "python-install-default", + "python-upgrade", + "json-output", + "pylock", + "add-bounds", + "package-conflicts", + "extra-build-dependencies", + "detect-module-conflicts", + "format-command", + "native-auth", + "s3-endpoint", + "cache-size", + "init-project-flag", + "workspace-metadata", + "workspace-dir", + "workspace-list", + "sbom-export", + "auth-helper", + "direct-publish", + "target-workspace-discovery", + "metadata-json", + "gcs-endpoint", + "adjust-ulimit", + "special-conda-env-names", + "relocatable-envs-default", + "publish-require-normalized", + "audit-command", + "project-directory-must-exist", + "index-exclude-newer", + "azure-endpoint", + "toml-backwards-compatibility", + "malware-check", + "venv-safe-clear", + "check-command", + "packaged-init" + ] + }, + { + "pattern": "\\S" + } + ] + }, + "PreviewFeaturesOption": { + "description": "Represents the `preview-features` configuration option.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/PreviewFeature" + } + } + ] + }, "ProxyUrl": { "description": "A proxy URL (e.g., `http://proxy.example.com:8080`).", "type": "string",