From 46bf1ff781a21c71fa13f3c2cee9a4fae4c46c17 Mon Sep 17 00:00:00 2001 From: j-helland Date: Sat, 25 Oct 2025 18:34:21 -0600 Subject: [PATCH 1/3] Added support for [tool.uv.preview-features] in TOML configs Preview features are specified as a list of strings in TOML, which parse to PreviewFeatures values. CLI arguments like --preview and --no-preview take precedence over config values. --preview-features combines with config values. --- Cargo.lock | 3 + crates/uv-preview/Cargo.toml | 2 + crates/uv-preview/src/lib.rs | 123 +++- crates/uv-settings/Cargo.toml | 1 + crates/uv-settings/src/combine.rs | 2 + crates/uv-settings/src/lib.rs | 4 + crates/uv-settings/src/settings.rs | 15 +- crates/uv/src/settings.rs | 32 +- crates/uv/tests/it/show_settings.rs | 1000 ++++++++++++++++++++++++++- uv.schema.json | 35 +- 10 files changed, 1177 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dab67119..73ab3379c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6436,6 +6436,8 @@ name = "uv-preview" version = "0.0.7" dependencies = [ "bitflags 2.9.4", + "schemars", + "serde", "thiserror 2.0.17", "uv-warnings", ] @@ -6767,6 +6769,7 @@ dependencies = [ "uv-normalize", "uv-options-metadata", "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 42c9ff3bc..553fafec4 100644 --- a/crates/uv-preview/Cargo.toml +++ b/crates/uv-preview/Cargo.toml @@ -19,6 +19,8 @@ workspace = true uv-warnings = { workspace = true } bitflags = { workspace = true } +schemars = { workspace = true, optional = true } +serde = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 146c6e580..fa8c2b6da 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "schemars")] +use std::borrow::Cow; use std::{ fmt::{Display, Formatter}, str::FromStr, @@ -70,6 +72,57 @@ impl Display for PreviewFeatures { } } +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for PreviewFeatures { + fn schema_name() -> Cow<'static, str> { + Cow::Borrowed("PreviewFeatures") + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + let choices: Vec<&str> = Self::all().iter().map(Self::flag_as_str).collect(); + schemars::json_schema!({ + "type": "string", + "enum": choices, + }) + } +} + +impl<'de> serde::Deserialize<'de> for PreviewFeatures { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl serde::de::Visitor<'_> for Visitor { + type Value = PreviewFeatures; + + fn expecting(&self, f: &mut Formatter) -> std::fmt::Result { + f.write_str("a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + PreviewFeatures::from_str(v).map_err(serde::de::Error::custom) + } + } + + deserializer.deserialize_str(Visitor) + } +} + +impl serde::Serialize for PreviewFeatures { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let features: Vec<&str> = self.iter().map(Self::flag_as_str).collect(); + features.serialize(serializer) + } +} + #[derive(Debug, Error, Clone)] pub enum PreviewFeaturesParseError { #[error("Empty string in preview features: {0}")] @@ -121,6 +174,28 @@ impl FromStr for PreviewFeatures { } } +pub enum PreviewFeaturesMode { + EnableAll, + DisableAll, + Selection(PreviewFeatures), +} + +impl PreviewFeaturesMode { + pub fn from_bool(b: bool) -> Self { + if b { Self::EnableAll } else { Self::DisableAll } + } +} + +impl<'a, I> From for PreviewFeaturesMode +where + I: Iterator, +{ + fn from(features: I) -> Self { + let flags = features.fold(PreviewFeatures::empty(), |f1, f2| f1 | *f2); + Self::Selection(flags) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Preview { flags: PreviewFeatures, @@ -135,33 +210,21 @@ impl Preview { Self::new(PreviewFeatures::all()) } - pub fn from_args( - preview: bool, - no_preview: bool, - preview_features: &[PreviewFeatures], - ) -> Self { - if no_preview { - return Self::default(); - } - - if preview { - return Self::all(); - } - - let mut flags = PreviewFeatures::empty(); - - for features in preview_features { - flags |= *features; - } - - Self { flags } - } - pub fn is_enabled(&self, flag: PreviewFeatures) -> bool { self.flags.contains(flag) } } +impl From for Preview { + fn from(mode: PreviewFeaturesMode) -> Self { + match mode { + PreviewFeaturesMode::EnableAll => Self::all(), + PreviewFeaturesMode::DisableAll => Self::default(), + PreviewFeaturesMode::Selection(flags) => Self { flags }, + } + } +} + impl Display for Preview { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.flags.is_empty() { @@ -239,19 +302,21 @@ mod tests { #[test] fn test_preview_from_args() { // Test no_preview - let preview = Preview::from_args(true, true, &[]); + let preview = Preview::from(PreviewFeaturesMode::DisableAll); assert_eq!(preview.to_string(), "disabled"); // Test preview (all features) - let preview = Preview::from_args(true, false, &[]); + let preview = Preview::from(PreviewFeaturesMode::EnableAll); assert_eq!(preview.to_string(), "enabled"); // Test specific features - let features = vec![ - PreviewFeatures::PYTHON_UPGRADE, - PreviewFeatures::JSON_OUTPUT, - ]; - let preview = Preview::from_args(false, false, &features); + let preview = Preview::from(PreviewFeaturesMode::from( + [ + PreviewFeatures::PYTHON_UPGRADE, + PreviewFeatures::JSON_OUTPUT, + ] + .iter(), + )); assert!(preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE)); assert!(preview.is_enabled(PreviewFeatures::JSON_OUTPUT)); assert!(!preview.is_enabled(PreviewFeatures::PYLOCK)); diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index 80a6a2fc8..8d1de0463 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -28,6 +28,7 @@ uv-macros = { workspace = true } uv-normalize = { workspace = true, features = ["schemars"] } uv-options-metadata = { workspace = true } uv-pep508 = { workspace = true } +uv-preview = { workspace = true, features = ["schemars"] } uv-pypi-types = { workspace = true } uv-python = { workspace = true, features = ["schemars", "clap"] } uv-redacted = { workspace = true } diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 15a63e1dc..3b26ba55f 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -12,6 +12,7 @@ use uv_distribution_types::{ PipFindLinks, PipIndex, }; use uv_install_wheel::LinkMode; +use uv_preview::PreviewFeatures; use uv_pypi_types::{SchemaConflicts, SupportedEnvironments}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_redacted::DisplaySafeUrl; @@ -100,6 +101,7 @@ impl_combine_or!(PipExtraIndex); impl_combine_or!(PipFindLinks); impl_combine_or!(PipIndex); impl_combine_or!(PrereleaseMode); +impl_combine_or!(PreviewFeatures); impl_combine_or!(PythonDownloads); impl_combine_or!(PythonPreference); impl_combine_or!(PythonVersion); diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index c8383e048..e90105cee 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -297,6 +297,7 @@ fn warn_uv_toml_masked_fields(options: &Options) { no_cache, cache_dir, preview, + preview_features, python_preference, python_downloads, concurrent_downloads, @@ -389,6 +390,9 @@ fn warn_uv_toml_masked_fields(options: &Options) { if preview.is_some() { masked_fields.push("preview"); } + if preview_features.is_some() { + masked_fields.push("preview-features"); + } 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 7169f32d2..c2e3c5e15 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -15,6 +15,7 @@ use uv_install_wheel::LinkMode; use uv_macros::{CombineOptions, OptionsMetadata}; use uv_normalize::{ExtraName, PackageName, PipGroupName}; use uv_pep508::Requirement; +use uv_preview::PreviewFeatures; use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_redacted::DisplaySafeUrl; @@ -248,7 +249,7 @@ pub struct GlobalOptions { "# )] pub cache_dir: Option, - /// Whether to enable experimental, preview features. + /// Whether to enable all experimental, preview features. #[option( default = "false", value_type = "bool", @@ -257,6 +258,15 @@ pub struct GlobalOptions { "# )] pub preview: Option, + /// Whether to enable specific experimental, preview features. + #[option( + default = "[]", + value_type = "list[str]", + example = r#" + preview-features = ["python-upgrade"] + "# + )] + pub preview_features: Option>, /// Whether to prefer using Python installations that are already present on the system, or /// those that are downloaded and installed by uv. #[option( @@ -2048,6 +2058,7 @@ pub struct OptionsWire { no_cache: Option, cache_dir: Option, preview: Option, + preview_features: Option>, python_preference: Option, python_downloads: Option, concurrent_downloads: Option, @@ -2142,6 +2153,7 @@ impl From for Options { no_cache, cache_dir, preview, + preview_features, python_preference, python_downloads, python_install_mirror, @@ -2213,6 +2225,7 @@ impl From for Options { no_cache, cache_dir, preview, + preview_features, python_preference, python_downloads, concurrent_downloads, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index bf879f7b4..844d8acb9 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -38,7 +38,7 @@ use uv_distribution_types::{ use uv_install_wheel::LinkMode; use uv_normalize::{ExtraName, PackageName, PipGroupName}; use uv_pep508::{MarkerTree, RequirementOrigin}; -use uv_preview::Preview; +use uv_preview::{Preview, PreviewFeaturesMode}; use uv_pypi_types::SupportedEnvironments; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; use uv_redacted::DisplaySafeUrl; @@ -139,13 +139,7 @@ impl GlobalSettings { .unwrap_or_else(Concurrency::threads), }, show_settings: args.show_settings, - preview: Preview::from_args( - flag(args.preview, args.no_preview, "preview") - .combine(workspace.and_then(|workspace| workspace.globals.preview)) - .unwrap_or(false), - args.no_preview, - &args.preview_features, - ), + preview: Preview::from(resolve_preview_settings(args, workspace)), python_preference, python_downloads: flag( args.allow_python_downloads, @@ -179,6 +173,28 @@ fn resolve_python_preference( } } +fn resolve_preview_settings( + args: &GlobalArgs, + workspace: Option<&FilesystemOptions>, +) -> PreviewFeaturesMode { + if let Some(preview) = flag(args.preview, args.no_preview, "preview") { + return PreviewFeaturesMode::from_bool(preview); + } + match workspace.and_then(|workspace| workspace.globals.preview) { + Some(true) => PreviewFeaturesMode::EnableAll, + // Disable workspace `preview-features` but keep any CLI `--preview-features` + Some(false) => PreviewFeaturesMode::from(args.preview_features.iter()), + None => { + let features = args.preview_features.iter().chain( + workspace + .and_then(|workspace| workspace.globals.preview_features.as_deref()) + .unwrap_or_default(), + ); + PreviewFeaturesMode::from(features) + } + } +} + /// The resolved network settings to use for any invocation of the CLI. #[derive(Debug, Clone)] pub(crate) struct NetworkSettings { diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 3bc7e011c..488cae9f0 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -4684,7 +4684,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `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`, `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`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `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`, `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`, `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`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `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` " ); @@ -8495,6 +8495,1004 @@ fn preview_features() { ); } +/// Test CLI preview feature flags vs config precedence. +/// The CLI should always have higher priority. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn preview_features_precedence() -> anyhow::Result<()> { + let context = TestContext::new("3.12"); + + let cmd = || { + let mut cmd = context.version(); + cmd.arg("--show-settings"); + add_shared_args(cmd, context.temp_dir.path()) + }; + + let config = context.temp_dir.child("pyproject.toml"); + config.write_str(indoc::indoc! {r#" + [project] + name = "demo" + version = "0.1.0" + + [tool.uv] + preview = false + preview-features = ["format"] + "#})?; + + // `uv.tool.preview = false` disables all features regardless of `uv.tool.preview-features`. + uv_snapshot!(context.filters(), cmd(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + timeout: [TIME], + retries: 3, + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + lock_check: Disabled, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: ExcludeNewer { + global: None, + package: ExcludeNewerPackage( + {}, + ), + }, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + build_isolation: Isolate, + extra_build_dependencies: ExtraBuildDependencies( + {}, + ), + extra_build_variables: ExtraBuildVariables( + {}, + ), + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + // `tool.uv.preview-features` will not merge with CLI `--preview-features` + // if `tool.uv.preview = false` + uv_snapshot!(context.filters(), cmd() + .arg("--preview-features") + .arg("pylock"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + timeout: [TIME], + retries: 3, + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + PYLOCK, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + lock_check: Disabled, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: ExcludeNewer { + global: None, + package: ExcludeNewerPackage( + {}, + ), + }, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + build_isolation: Isolate, + extra_build_dependencies: ExtraBuildDependencies( + {}, + ), + extra_build_variables: ExtraBuildVariables( + {}, + ), + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + // CLI `--preview` takes precedence over configs settings. + uv_snapshot!(context.filters(), cmd().arg("--preview") , @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + timeout: [TIME], + retries: 3, + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + lock_check: Disabled, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: ExcludeNewer { + global: None, + package: ExcludeNewerPackage( + {}, + ), + }, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + build_isolation: Isolate, + extra_build_dependencies: ExtraBuildDependencies( + {}, + ), + extra_build_variables: ExtraBuildVariables( + {}, + ), + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + let config = context.temp_dir.child("pyproject.toml"); + config.write_str( + r#" + [project] + name = "demo" + version = "0.1.0" + + [tool.uv] + preview = true + preview-features = ["format"] + "#, + )?; + + // `uv.tool.preview = true` enables all features, regardless of `uv.tool.preview-features` + uv_snapshot!(context.filters(), cmd(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + timeout: [TIME], + retries: 3, + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + lock_check: Disabled, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: ExcludeNewer { + global: None, + package: ExcludeNewerPackage( + {}, + ), + }, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + build_isolation: Isolate, + extra_build_dependencies: ExtraBuildDependencies( + {}, + ), + extra_build_variables: ExtraBuildVariables( + {}, + ), + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + // CLI `--preview-features` merges into `uv.tool.preview = true`, resulting in all features + // being enabled. + uv_snapshot!(context.filters(), cmd() + .arg("--preview-features") + .arg("pylock"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + timeout: [TIME], + retries: 3, + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + lock_check: Disabled, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: ExcludeNewer { + global: None, + package: ExcludeNewerPackage( + {}, + ), + }, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + build_isolation: Isolate, + extra_build_dependencies: ExtraBuildDependencies( + {}, + ), + extra_build_variables: ExtraBuildVariables( + {}, + ), + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + // CLI `--no-preview` takes precedence over config settings. + uv_snapshot!(context.filters(), cmd().arg("--no-preview"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + timeout: [TIME], + retries: 3, + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + lock_check: Disabled, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: ExcludeNewer { + global: None, + package: ExcludeNewerPackage( + {}, + ), + }, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + build_isolation: Isolate, + extra_build_dependencies: ExtraBuildDependencies( + {}, + ), + extra_build_variables: ExtraBuildVariables( + {}, + ), + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + let config = context.temp_dir.child("pyproject.toml"); + config.write_str( + r#" + [project] + name = "demo" + version = "0.1.0" + + [tool.uv] + preview-features = ["format"] + "#, + )?; + + // CLI `--preview-features` combines with `uv.tool.preview-features` + uv_snapshot!(context.filters(), cmd() + .arg("--preview-features") + .arg("pylock"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + timeout: [TIME], + retries: 3, + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + PYLOCK | FORMAT, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + lock_check: Disabled, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: ExcludeNewer { + global: None, + package: ExcludeNewerPackage( + {}, + ), + }, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + build_isolation: Isolate, + extra_build_dependencies: ExtraBuildDependencies( + {}, + ), + extra_build_variables: ExtraBuildVariables( + {}, + ), + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + let config = context.temp_dir.child("pyproject.toml"); + config.write_str( + r#" + [project] + name = "demo" + version = "0.1.0" + + [tool.uv] + preview-features = [ + "format", + "format", + ] + "#, + )?; + + // Duplicate preview features are reduced + uv_snapshot!(context.filters(), cmd(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + timeout: [TIME], + retries: 3, + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + FORMAT, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + lock_check: Disabled, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: ExcludeNewer { + global: None, + package: ExcludeNewerPackage( + {}, + ), + }, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + build_isolation: Isolate, + extra_build_dependencies: ExtraBuildDependencies( + {}, + ), + extra_build_variables: ExtraBuildVariables( + {}, + ), + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + Ok(()) +} + /// Track the interactions between `upgrade` and `upgrade-package` across the `uv pip` CLI and a /// configuration file. #[test] diff --git a/uv.schema.json b/uv.schema.json index bcabfdfd1..9b2333209 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -475,12 +475,22 @@ ] }, "preview": { - "description": "Whether to enable experimental, preview features.", + "description": "Whether to enable all experimental, preview features.", "type": [ "boolean", "null" ] }, + "preview-features": { + "description": "Whether to enable specific experimental, preview features.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/PreviewFeatures" + } + }, "publish-url": { "description": "The URL for publishing packages to the Python package index (by default:\n).", "anyOf": [ @@ -1793,6 +1803,29 @@ } ] }, + "PreviewFeatures": { + "type": "string", + "enum": [ + "python-install-default", + "python-upgrade", + "json-output", + "pylock", + "add-bounds", + "package-conflicts", + "extra-build-dependencies", + "detect-module-conflicts", + "format", + "native-auth", + "s3-endpoint", + "cache-size", + "init-project-flag", + "workspace-metadata", + "workspace-dir", + "workspace-list", + "sbom-export", + "auth-helper" + ] + }, "PythonDownloads": { "oneOf": [ { From 9232ece843a60fd1eb97346f4744e88c314ee1b9 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 18 Dec 2025 07:59:41 -0600 Subject: [PATCH 2/3] Add roundtrip test case --- Cargo.lock | 2 ++ crates/uv-preview/Cargo.toml | 2 ++ crates/uv-preview/src/lib.rs | 16 ++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index faa753040..01305faf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6454,8 +6454,10 @@ name = "uv-preview" version = "0.0.8" dependencies = [ "bitflags 2.9.4", + "insta", "schemars", "serde", + "serde_json", "thiserror 2.0.17", "uv-warnings", ] diff --git a/crates/uv-preview/Cargo.toml b/crates/uv-preview/Cargo.toml index 25072182b..67dc76f57 100644 --- a/crates/uv-preview/Cargo.toml +++ b/crates/uv-preview/Cargo.toml @@ -24,6 +24,8 @@ serde = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +insta = { workspace = true } +serde_json = { workspace = true } [features] default = [] diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index fa8c2b6da..50f992f42 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -358,4 +358,20 @@ mod tests { let features = PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::JSON_OUTPUT; let _ = features.flag_as_str(); } + + #[test] + fn test_serde_roundtrip() { + let input = r#"["python-upgrade", "format"]"#; + + let deserialized: Vec = serde_json::from_str(input).unwrap(); + assert_eq!(deserialized.len(), 2); + assert_eq!(deserialized[0], PreviewFeatures::PYTHON_UPGRADE); + assert_eq!(deserialized[1], PreviewFeatures::FORMAT); + + let serialized = serde_json::to_string(&deserialized).unwrap(); + insta::assert_snapshot!(serialized, @r#"["python-upgrade","format"]"#); + + let roundtrip: Vec = serde_json::from_str(&serialized).unwrap(); + assert_eq!(roundtrip, deserialized); + } } From 7b2c211726fddee212dc80b0f34067ad09ac157a Mon Sep 17 00:00:00 2001 From: j-helland Date: Fri, 19 Dec 2025 21:18:40 -0700 Subject: [PATCH 3/3] Make preview feature commandline args take precedence over config file fields * Instead of merging commandline `--preview-features` with config file `preview-features`, now the commandline arguments take precedence. This matches the semantics of how uv handles other commandline arguments. * Removed unnecessary `PreviewFeaturesMode` type. * Fixed bug with `PreviewFeatures` serde serializer that produced a list of lists instead of a flat list as expected. --- crates/uv-preview/src/lib.rs | 80 ++++++++++++++--------------- crates/uv/src/settings.rs | 38 +++++++------- crates/uv/tests/it/show_settings.rs | 9 ++-- uv.schema.json | 10 +--- 4 files changed, 65 insertions(+), 72 deletions(-) diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 50f992f42..30e02fac2 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -118,8 +118,18 @@ impl serde::Serialize for PreviewFeatures { where S: serde::Serializer, { - let features: Vec<&str> = self.iter().map(Self::flag_as_str).collect(); - features.serialize(serializer) + if *self == Self::default() { + return Err(serde::ser::Error::custom( + "Cannot serialize disabled feature flags.", + )); + } + if self.bits().count_ones() > 1 { + return Err(serde::ser::Error::custom( + "More than one preview feature flag enabled. \ + Only individual flags should be serialized at a time.", + )); + } + serializer.serialize_str(self.flag_as_str()) } } @@ -174,28 +184,6 @@ impl FromStr for PreviewFeatures { } } -pub enum PreviewFeaturesMode { - EnableAll, - DisableAll, - Selection(PreviewFeatures), -} - -impl PreviewFeaturesMode { - pub fn from_bool(b: bool) -> Self { - if b { Self::EnableAll } else { Self::DisableAll } - } -} - -impl<'a, I> From for PreviewFeaturesMode -where - I: Iterator, -{ - fn from(features: I) -> Self { - let flags = features.fold(PreviewFeatures::empty(), |f1, f2| f1 | *f2); - Self::Selection(flags) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Preview { flags: PreviewFeatures, @@ -215,13 +203,19 @@ impl Preview { } } -impl From for Preview { - fn from(mode: PreviewFeaturesMode) -> Self { - match mode { - PreviewFeaturesMode::EnableAll => Self::all(), - PreviewFeaturesMode::DisableAll => Self::default(), - PreviewFeaturesMode::Selection(flags) => Self { flags }, - } +impl<'flag> FromIterator<&'flag PreviewFeatures> for Preview { + fn from_iter>(iter: T) -> Self { + let flags = iter + .into_iter() + .copied() + .fold(PreviewFeatures::empty(), |f1, f2| f1 | f2); + Self::new(flags) + } +} + +impl From for Preview { + fn from(value: bool) -> Self { + if value { Self::all() } else { Self::default() } } } @@ -302,21 +296,20 @@ mod tests { #[test] fn test_preview_from_args() { // Test no_preview - let preview = Preview::from(PreviewFeaturesMode::DisableAll); + let preview = Preview::default(); assert_eq!(preview.to_string(), "disabled"); // Test preview (all features) - let preview = Preview::from(PreviewFeaturesMode::EnableAll); + let preview = Preview::all(); assert_eq!(preview.to_string(), "enabled"); // Test specific features - let preview = Preview::from(PreviewFeaturesMode::from( - [ - PreviewFeatures::PYTHON_UPGRADE, - PreviewFeatures::JSON_OUTPUT, - ] - .iter(), - )); + let preview: Preview = [ + PreviewFeatures::PYTHON_UPGRADE, + PreviewFeatures::JSON_OUTPUT, + ] + .iter() + .collect(); assert!(preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE)); assert!(preview.is_enabled(PreviewFeatures::JSON_OUTPUT)); assert!(!preview.is_enabled(PreviewFeatures::PYLOCK)); @@ -374,4 +367,11 @@ mod tests { let roundtrip: Vec = serde_json::from_str(&serialized).unwrap(); assert_eq!(roundtrip, deserialized); } + + #[test] + #[should_panic(expected = "Cannot serialize disabled feature flags.")] + fn test_serialize_default() { + let disabled = PreviewFeatures::default(); + let _ = serde_json::to_string(&disabled).unwrap(); + } } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 97e0e1bd9..4e61a6574 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -38,7 +38,7 @@ use uv_distribution_types::{ use uv_install_wheel::LinkMode; use uv_normalize::{ExtraName, PackageName, PipGroupName}; use uv_pep508::{MarkerTree, RequirementOrigin}; -use uv_preview::{Preview, PreviewFeaturesMode}; +use uv_preview::Preview; use uv_pypi_types::SupportedEnvironments; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; use uv_redacted::DisplaySafeUrl; @@ -139,7 +139,7 @@ impl GlobalSettings { .unwrap_or_else(Concurrency::threads), }, show_settings: args.show_settings, - preview: Preview::from(resolve_preview_settings(args, workspace)), + preview: resolve_preview_settings(args, workspace), python_preference, python_downloads: flag( args.allow_python_downloads, @@ -173,26 +173,26 @@ fn resolve_python_preference( } } -fn resolve_preview_settings( - args: &GlobalArgs, - workspace: Option<&FilesystemOptions>, -) -> PreviewFeaturesMode { +fn resolve_preview_settings(args: &GlobalArgs, workspace: Option<&FilesystemOptions>) -> Preview { + // Commandline arguments take priority. if let Some(preview) = flag(args.preview, args.no_preview, "preview") { - return PreviewFeaturesMode::from_bool(preview); + return Preview::from(preview); } - match workspace.and_then(|workspace| workspace.globals.preview) { - Some(true) => PreviewFeaturesMode::EnableAll, - // Disable workspace `preview-features` but keep any CLI `--preview-features` - Some(false) => PreviewFeaturesMode::from(args.preview_features.iter()), - None => { - let features = args.preview_features.iter().chain( + if !args.preview_features.is_empty() { + return Preview::from_iter(&args.preview_features); + } + + workspace + .and_then(|workspace| { + workspace.globals.preview.map(Preview::from).or_else(|| { workspace - .and_then(|workspace| workspace.globals.preview_features.as_deref()) - .unwrap_or_default(), - ); - PreviewFeaturesMode::from(features) - } - } + .globals + .preview_features + .as_ref() + .map(Preview::from_iter) + }) + }) + .unwrap_or_default() } /// The resolved network settings to use for any invocation of the CLI. diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index fa84e3b91..60bedafe3 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -9010,8 +9010,7 @@ fn preview_features_precedence() -> anyhow::Result<()> { "# ); - // CLI `--preview-features` merges into `uv.tool.preview = true`, resulting in all features - // being enabled. + // CLI `--preview-features` takes precedence over `uv.tool.preview = true`. uv_snapshot!(context.filters(), cmd() .arg("--preview-features") .arg("pylock"), @r#" @@ -9038,7 +9037,7 @@ fn preview_features_precedence() -> anyhow::Result<()> { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER, + PYLOCK, ), }, python_preference: Managed, @@ -9257,7 +9256,7 @@ fn preview_features_precedence() -> anyhow::Result<()> { "#, )?; - // CLI `--preview-features` combines with `uv.tool.preview-features` + // CLI `--preview-features` takes precedence over `uv.tool.preview-features` uv_snapshot!(context.filters(), cmd() .arg("--preview-features") .arg("pylock"), @r#" @@ -9284,7 +9283,7 @@ fn preview_features_precedence() -> anyhow::Result<()> { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYLOCK | FORMAT, + PYLOCK, ), }, python_preference: Managed, diff --git a/uv.schema.json b/uv.schema.json index 0e0737734..859f4993f 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -386,17 +386,11 @@ }, "preview": { "description": "Whether to enable all experimental, preview features.", - "type": [ - "boolean", - "null" - ] + "type": ["boolean", "null"] }, "preview-features": { "description": "Whether to enable specific experimental, preview features.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/PreviewFeatures" }