Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/uv-build-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1810,7 +1810,7 @@ mod tests {
let build = build(
src.path(),
dist.path(),
Preview::new(&[PreviewFeature::MetadataJson]),
Preview::from_iter(&[PreviewFeature::MetadataJson]),
)
.unwrap();

Expand Down
4 changes: 4 additions & 0 deletions crates/uv-preview/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ workspace = true
[dependencies]
uv-warnings = { workspace = true }

schemars = { workspace = true, optional = true }
serde = { workspace = true }
enumflags2 = { workspace = true }
itertools = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
insta = { workspace = true }
serde_json = { workspace = true }

[features]
default = []
104 changes: 75 additions & 29 deletions crates/uv-preview/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::{
fmt::{Debug, Display, Formatter},
ops::BitOr,
Expand Down Expand Up @@ -117,6 +118,40 @@ impl FromStr for PreviewFeature {
}
}

#[cfg(feature = "schemars")]
impl schemars::JsonSchema for PreviewFeature {
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("PreviewFeature")
}

fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let choices: Vec<&str> = BitFlags::<Self>::all().iter().map(Self::as_str).collect();
schemars::json_schema!({
"type": "string",
"enum": choices,
})
}
}

impl<'de> serde::Deserialize<'de> for PreviewFeature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}

impl serde::Serialize for PreviewFeature {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}

#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub struct Preview {
flags: BitFlags<PreviewFeature>,
Expand All @@ -130,36 +165,34 @@ impl Debug for Preview {
}

impl Preview {
pub fn new(flags: &[PreviewFeature]) -> Self {
Self {
flags: flags.iter().copied().fold(BitFlags::empty(), BitOr::bitor),
}
}

pub fn all() -> Self {
Self {
flags: BitFlags::all(),
}
}

pub fn from_args(preview: bool, no_preview: bool, preview_features: &[PreviewFeature]) -> Self {
if no_preview {
return Self::default();
}

if preview {
return Self::all();
}

Self::new(preview_features)
}

/// Check if a single feature is enabled
pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
self.flags.contains(flag)
}
}

impl<'flag> FromIterator<&'flag PreviewFeature> for Preview {
fn from_iter<T: IntoIterator<Item = &'flag PreviewFeature>>(iter: T) -> Self {
let flags = iter
.into_iter()
.copied()
.fold(BitFlags::empty(), BitOr::bitor);
Self { 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() {
Expand Down Expand Up @@ -251,39 +284,36 @@ mod tests {
// Test disabled
let preview = Preview::default();
assert_eq!(preview.to_string(), "disabled");
let preview = Preview::new(&[]);
let preview = Preview::from_iter(&[]);
assert_eq!(preview.to_string(), "disabled");

// Test enabled (all features)
let preview = Preview::all();
assert_eq!(preview.to_string(), "enabled");

// Test single feature
let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]);
let preview = Preview::from_iter(&[PreviewFeature::PythonInstallDefault]);
assert_eq!(preview.to_string(), "python-install-default");

// Test multiple features
let preview = Preview::new(&[PreviewFeature::PythonUpgrade, PreviewFeature::Pylock]);
let preview = Preview::from_iter(&[PreviewFeature::PythonUpgrade, PreviewFeature::Pylock]);
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, &[]);
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![PreviewFeature::PythonUpgrade, PreviewFeature::JsonOutput];
let preview = Preview::from_args(false, false, &features);
let preview: Preview = [PreviewFeature::PythonUpgrade, PreviewFeature::JsonOutput]
.iter()
.collect();
assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
assert!(preview.is_enabled(PreviewFeature::JsonOutput));
assert!(!preview.is_enabled(PreviewFeature::Pylock));
Expand Down Expand Up @@ -344,4 +374,20 @@ mod tests {
"relocatable-envs-default"
);
}

#[test]
fn test_serde_roundtrip() {
let input = r#"["python-upgrade", "format"]"#;

let deserialized: Vec<PreviewFeature> = serde_json::from_str(input).unwrap();
assert_eq!(deserialized.len(), 2);
assert_eq!(deserialized[0], PreviewFeature::PythonUpgrade);
assert_eq!(deserialized[1], PreviewFeature::Format);

let serialized = serde_json::to_string(&deserialized).unwrap();
insta::assert_snapshot!(serialized, @r#"["python-upgrade","format"]"#);

let roundtrip: Vec<PreviewFeature> = serde_json::from_str(&serialized).unwrap();
assert_eq!(roundtrip, deserialized);
}
}
2 changes: 1 addition & 1 deletion crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,7 @@ mod tests {
EnvironmentPreference::OnlyVirtual,
PythonPreference::OnlySystem,
&context.cache,
Preview::new(&[PreviewFeature::SpecialCondaEnvNames]),
Preview::from_iter(&[PreviewFeature::SpecialCondaEnvNames]),
)
},
)?
Expand Down
1 change: 1 addition & 0 deletions crates/uv-settings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-settings/src/combine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use uv_distribution_types::{
PipFindLinks, PipIndex,
};
use uv_install_wheel::LinkMode;
use uv_preview::Preview;
use uv_pypi_types::{SchemaConflicts, SupportedEnvironments};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_redacted::DisplaySafeUrl;
Expand Down Expand Up @@ -100,6 +101,7 @@ impl_combine_or!(PipExtraIndex);
impl_combine_or!(PipFindLinks);
impl_combine_or!(PipIndex);
impl_combine_or!(PrereleaseMode);
impl_combine_or!(Preview);
impl_combine_or!(ProxyUrl);
impl_combine_or!(PythonDownloads);
impl_combine_or!(PythonPreference);
Expand Down
4 changes: 4 additions & 0 deletions crates/uv-settings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -394,6 +395,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");
}
Expand Down
15 changes: 14 additions & 1 deletion crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::PreviewFeature;
use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_redacted::DisplaySafeUrl;
Expand Down Expand Up @@ -250,7 +251,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",
Expand All @@ -259,6 +260,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<PreviewFeature>>,
/// Whether to prefer using Python installations that are already present on the system, or
/// those that are downloaded and installed by uv.
#[option(
Expand Down Expand Up @@ -2153,6 +2163,7 @@ pub struct OptionsWire {
no_cache: Option<bool>,
cache_dir: Option<PathBuf>,
preview: Option<bool>,
preview_features: Option<Vec<PreviewFeature>>,
python_preference: Option<PythonPreference>,
python_downloads: Option<PythonDownloads>,
concurrent_downloads: Option<NonZeroUsize>,
Expand Down Expand Up @@ -2252,6 +2263,7 @@ impl From<OptionsWire> for Options {
no_cache,
cache_dir,
preview,
preview_features,
python_preference,
python_downloads,
python_install_mirror,
Expand Down Expand Up @@ -2328,6 +2340,7 @@ impl From<OptionsWire> for Options {
no_cache,
cache_dir,
preview,
preview_features,
python_preference,
python_downloads,
concurrent_downloads,
Expand Down
48 changes: 28 additions & 20 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,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,
Expand Down Expand Up @@ -217,22 +213,34 @@ 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 {
// Commandline arguments take priority.
if let Some(preview) = flag(args.preview, args.no_preview, "preview") {
return preview.into();
}

// Check environment variable
if let Some(true) = environment.preview.value {
return Preview::all();
}

// From preview_features
if !args.preview_features.is_empty() {
return Preview::from_iter(&args.preview_features);
}

// Fall back to workspace config
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.
Expand Down
Loading