From a697a6b5443a13a61656ff3a9ec2b1fd4394ae28 Mon Sep 17 00:00:00 2001 From: "Tomasz (Tom) Kramkowski" Date: Wed, 3 Jun 2026 22:44:48 +0100 Subject: [PATCH 1/3] Restore leniency for unknown preview features --- crates/uv-cli/src/lib.rs | 4 +- crates/uv-preview/src/lib.rs | 94 ++++++++++++++++++++--------- crates/uv/tests/it/show_settings.rs | 38 ++++++++++++ 3 files changed, 105 insertions(+), 31 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 0894ebaf6a985..3f67453ca1237 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -24,7 +24,7 @@ use uv_distribution_types::{ }; use uv_normalize::{ExtraName, GroupName, PackageName, PipGroupName}; use uv_pep508::{MarkerTree, Requirement}; -use uv_preview::PreviewFeature; +use uv_preview::MaybePreviewFeature; use uv_pypi_types::VerbatimParsedUrl; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; use uv_redacted::DisplaySafeUrl; @@ -328,7 +328,7 @@ pub struct GlobalArgs { alias = "preview-feature", value_enum, )] - pub preview_features: Vec, + pub preview_features: Vec, /// Avoid discovering a `pyproject.toml` or `uv.toml` file [env: UV_ISOLATED=] /// diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 8490eb0e041b5..42a27d7634f9c 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -354,6 +354,33 @@ impl FromStr for PreviewFeature { } } +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] +#[error("preview feature name cannot be empty")] +pub struct EmptyPreviewFeatureNameError; + +/// A user-provided preview feature name, which may refer to an unknown feature. +#[derive(Debug, Clone)] +pub enum MaybePreviewFeature { + Known(PreviewFeature), + Unknown(String), +} + +impl FromStr for MaybePreviewFeature { + type Err = EmptyPreviewFeatureNameError; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + if s.is_empty() { + return Err(EmptyPreviewFeatureNameError); + } + + Ok(match PreviewFeature::from_str(s) { + Ok(feature) => Self::Known(feature), + Err(_) => Self::Unknown(s.to_string()), + }) + } +} + #[derive(Clone, Copy, PartialEq, Eq, Default)] pub struct Preview { flags: BitFlags, @@ -379,7 +406,12 @@ impl Preview { } } - pub fn from_args(preview: bool, no_preview: bool, preview_features: &[PreviewFeature]) -> Self { + /// 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(); } @@ -388,7 +420,7 @@ impl Preview { return Self::all(); } - Self::new(preview_features) + Self::from_feature_names(preview_features) } /// Check if a single feature is enabled. @@ -405,6 +437,23 @@ impl Preview { pub fn any_enabled(&self) -> bool { !self.flags.is_empty() } + + fn from_feature_names<'a>( + feature_names: impl IntoIterator, + ) -> Self { + let mut flags = BitFlags::empty(); + + for feature_name in feature_names { + match feature_name { + MaybePreviewFeature::Known(feature) => flags |= *feature, + MaybePreviewFeature::Unknown(feature_name) => { + warn_user_once!("Unknown preview feature: `{feature_name}`"); + } + } + } + + Self { flags } + } } impl Display for Preview { @@ -423,35 +472,16 @@ impl Display for Preview { } } -#[derive(Debug, Error, Clone)] -pub enum PreviewParseError { - #[error("Empty string in preview features: {0}")] - Empty(String), -} - impl FromStr for Preview { - type Err = PreviewParseError; + type Err = EmptyPreviewFeatureNameError; fn from_str(s: &str) -> Result { - let mut flags = BitFlags::empty(); - - for part in s.split(',') { - let part = part.trim(); - if part.is_empty() { - return Err(PreviewParseError::Empty( - "Empty string in preview features".to_string(), - )); - } + let feature_names = s + .split(',') + .map(MaybePreviewFeature::from_str) + .collect::, _>>()?; - match PreviewFeature::from_str(part) { - Ok(flag) => flags |= flag, - Err(_) => { - warn_user_once!("Unknown preview feature: `{part}`"); - } - } - } - - Ok(Self { flags }) + Ok(Self::from_feature_names(&feature_names)) } } @@ -483,7 +513,7 @@ mod tests { assert!(preview.is_enabled(PreviewFeature::AddBounds)); // Test empty string error - assert!(Preview::from_str("").is_err()); + assert_eq!(Preview::from_str(""), Err(EmptyPreviewFeatureNameError)); assert!(Preview::from_str("pylock,").is_err()); assert!(Preview::from_str(",pylock").is_err()); @@ -529,11 +559,17 @@ mod tests { assert_eq!(preview.to_string(), "enabled"); // Test specific features - let features = vec![PreviewFeature::PythonUpgrade, PreviewFeature::JsonOutput]; + 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] diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index a57d4b5bc43bd..98808de7cfe7d 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -2967,6 +2967,44 @@ fn preview_features() { " ); + diff_uv_snapshot!( + context.filters(), + &preview_features, + add_shared_args(context.version()) + .arg("--show-settings") + .arg("--preview-features") + .arg("python-install-default,unknown-preview-feature,python-upgrade"), + @" + --- old + +++ new + @@ -122,3 +122,4 @@ + } + + ----- stderr ----- + +warning: Unknown preview feature: `unknown-preview-feature` + " + ); + + diff_uv_snapshot!( + context.filters(), + &preview_features, + add_shared_args(context.version()) + .arg("--show-settings") + .env( + EnvVars::UV_PREVIEW_FEATURES, + "python-install-default,unknown-preview-feature,python-upgrade", + ), + @" + --- old + +++ new + @@ -122,3 +122,4 @@ + } + + ----- stderr ----- + +warning: Unknown preview feature: `unknown-preview-feature` + " + ); + // Compare against output with both features passed to one `--preview-features` option. diff_uv_snapshot!(context.filters(), &preview_features, add_shared_args(context.version()).arg("--show-settings").arg("--preview-features").arg("python-install-default").arg("--preview-feature").arg("python-upgrade"), @"" ); From ed7bcb7e66c9fc5ca9c1fb81b1955381528ad2b1 Mon Sep 17 00:00:00 2001 From: "Tomasz (Tom) Kramkowski" Date: Wed, 3 Jun 2026 22:45:32 +0100 Subject: [PATCH 2/3] Test empty preview feature names --- crates/uv/tests/it/show_settings.rs | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 98808de7cfe7d..eb582573f5452 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -3017,6 +3017,44 @@ fn preview_features() { .arg("--no-preview"), @"" ); + + uv_snapshot!( + context.filters(), + add_shared_args(context.version()) + .arg("--show-settings") + .arg("--preview-features") + .arg("python-install-default,,python-upgrade"), + @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '' for '--preview-features ': preview feature name cannot be empty + + For more information, try '--help'. + " + ); + + uv_snapshot!( + context.filters(), + add_shared_args(context.version()) + .arg("--show-settings") + .env( + EnvVars::UV_PREVIEW_FEATURES, + "python-install-default,,python-upgrade", + ), + @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value '' for '--preview-features ': preview feature name cannot be empty + + For more information, try '--help'. + " + ); } #[test] From f88aef878433fdfced0a7c801e4573005154af39 Mon Sep 17 00:00:00 2001 From: "Tomasz (Tom) Kramkowski" Date: Wed, 3 Jun 2026 22:46:10 +0100 Subject: [PATCH 3/3] Remove ineffective preview feature value_enum --- crates/uv-cli/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 3f67453ca1237..609000758aec2 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -326,7 +326,6 @@ pub struct GlobalArgs { value_delimiter = ',', hide = true, alias = "preview-feature", - value_enum, )] pub preview_features: Vec,