This commit is contained in:
Jonathan Helland 2025-12-22 16:43:49 -05:00 committed by GitHub
commit 8e192c323c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1198 additions and 37 deletions

5
Cargo.lock generated
View file

@ -6454,6 +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",
]
@ -6787,6 +6791,7 @@ dependencies = [
"uv-normalize",
"uv-options-metadata",
"uv-pep508",
"uv-preview",
"uv-pypi-types",
"uv-python",
"uv-redacted",

View file

@ -19,9 +19,13 @@ workspace = true
uv-warnings = { workspace = true }
bitflags = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
insta = { workspace = true }
serde_json = { workspace = true }
[features]
default = []

View file

@ -1,3 +1,5 @@
#[cfg(feature = "schemars")]
use std::borrow::Cow;
use std::{
fmt::{Display, Formatter},
str::FromStr,
@ -70,6 +72,67 @@ 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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::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())
}
}
#[derive(Debug, Error, Clone)]
pub enum PreviewFeaturesParseError {
#[error("Empty string in preview features: {0}")]
@ -135,33 +198,27 @@ 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<'flag> FromIterator<&'flag PreviewFeatures> for Preview {
fn from_iter<T: IntoIterator<Item = &'flag PreviewFeatures>>(iter: T) -> Self {
let flags = iter
.into_iter()
.copied()
.fold(PreviewFeatures::empty(), |f1, f2| f1 | f2);
Self::new(flags)
}
}
impl From<bool> for Preview {
fn from(value: bool) -> Self {
if value { Self::all() } else { Self::default() }
}
}
impl Display for Preview {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.flags.is_empty() {
@ -239,19 +296,20 @@ mod tests {
#[test]
fn test_preview_from_args() {
// Test no_preview
let preview = Preview::from_args(true, true, &[]);
let preview = Preview::default();
assert_eq!(preview.to_string(), "disabled");
// Test preview (all features)
let preview = Preview::from_args(true, false, &[]);
let preview = Preview::all();
assert_eq!(preview.to_string(), "enabled");
// Test specific features
let features = vec![
let preview: Preview = [
PreviewFeatures::PYTHON_UPGRADE,
PreviewFeatures::JSON_OUTPUT,
];
let preview = Preview::from_args(false, false, &features);
]
.iter()
.collect();
assert!(preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE));
assert!(preview.is_enabled(PreviewFeatures::JSON_OUTPUT));
assert!(!preview.is_enabled(PreviewFeatures::PYLOCK));
@ -293,4 +351,27 @@ 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<PreviewFeatures> = 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<PreviewFeatures> = 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();
}
}

View file

@ -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 }

View file

@ -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);

View file

@ -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,
@ -390,6 +391,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");
}

View file

@ -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<PathBuf>,
/// 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<bool>,
/// Whether to enable specific experimental, preview features.
#[option(
default = "[]",
value_type = "list[str]",
example = r#"
preview-features = ["python-upgrade"]
"#
)]
pub preview_features: Option<Vec<PreviewFeatures>>,
/// Whether to prefer using Python installations that are already present on the system, or
/// those that are downloaded and installed by uv.
#[option(
@ -2090,6 +2100,7 @@ pub struct OptionsWire {
no_cache: Option<bool>,
cache_dir: Option<PathBuf>,
preview: Option<bool>,
preview_features: Option<Vec<PreviewFeatures>>,
python_preference: Option<PythonPreference>,
python_downloads: Option<PythonDownloads>,
concurrent_downloads: Option<NonZeroUsize>,
@ -2185,6 +2196,7 @@ impl From<OptionsWire> for Options {
no_cache,
cache_dir,
preview,
preview_features,
python_preference,
python_downloads,
python_install_mirror,
@ -2257,6 +2269,7 @@ impl From<OptionsWire> for Options {
no_cache,
cache_dir,
preview,
preview_features,
python_preference,
python_downloads,
concurrent_downloads,

View file

@ -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: 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>) -> Preview {
// Commandline arguments take priority.
if let Some(preview) = flag(args.preview, args.no_preview, "preview") {
return Preview::from(preview);
}
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
.globals
.preview_features
.as_ref()
.map(Preview::from_iter)
})
})
.unwrap_or_default()
}
/// The resolved network settings to use for any invocation of the CLI.
#[derive(Debug, Clone)]
pub(crate) struct NetworkSettings {

File diff suppressed because it is too large Load diff

32
uv.schema.json generated
View file

@ -385,9 +385,16 @@
]
},
"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<https://upload.pypi.org/legacy/>).",
"anyOf": [
@ -1521,6 +1528,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": [
{