Skip to content
Merged
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions crates/uv-bench/benches/uv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ mod resolver {
use uv_client::RegistryClient;
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, Constraints, IndexStrategy,
PackageConfigSettings, PreviewMode, SourceStrategy,
PackageConfigSettings, Preview, SourceStrategy,
};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::DistributionDatabase;
Expand Down Expand Up @@ -194,7 +194,7 @@ mod resolver {
sources,
workspace_cache,
concurrency,
PreviewMode::Enabled,
Preview::default(),
);

let markers = if universal {
Expand Down
4 changes: 2 additions & 2 deletions crates/uv-build-frontend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use tokio::sync::{Mutex, Semaphore};
use tracing::{Instrument, debug, info_span, instrument, warn};

use uv_cache_key::cache_digest;
use uv_configuration::PreviewMode;
use uv_configuration::Preview;
use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, SourceStrategy};
use uv_distribution::BuildRequires;
use uv_distribution_types::{IndexLocations, Requirement, Resolution};
Expand Down Expand Up @@ -286,7 +286,7 @@ impl SourceBuild {
mut environment_variables: FxHashMap<OsString, OsString>,
level: BuildOutput,
concurrent_builds: usize,
preview: PreviewMode,
preview: Preview,
) -> Result<Self, Error> {
let temp_dir = build_context.cache().venv_dir()?;

Expand Down
25 changes: 22 additions & 3 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use clap::{Args, Parser, Subcommand};
use uv_cache::CacheArgs;
use uv_configuration::{
ConfigSettingEntry, ConfigSettingPackageEntry, ExportFormat, IndexStrategy,
KeyringProviderType, PackageNameSpecifier, ProjectBuildBackend, TargetTriple, TrustedHost,
TrustedPublishing, VersionControlSystem,
KeyringProviderType, PackageNameSpecifier, PreviewFeatures, ProjectBuildBackend, TargetTriple,
TrustedHost, TrustedPublishing, VersionControlSystem,
};
use uv_distribution_types::{Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex};
use uv_normalize::{ExtraName, GroupName, PackageName, PipGroupName};
Expand Down Expand Up @@ -273,7 +273,7 @@ pub struct GlobalArgs {
)]
pub allow_insecure_host: Option<Vec<Maybe<TrustedHost>>>,

/// Whether to enable experimental, preview features.
/// Whether to enable all experimental preview features.
///
/// Preview features may change without warning.
#[arg(global = true, long, hide = true, env = EnvVars::UV_PREVIEW, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_preview"))]
Expand All @@ -282,6 +282,25 @@ pub struct GlobalArgs {
#[arg(global = true, long, overrides_with("preview"), hide = true)]
pub no_preview: bool,

/// Enable experimental preview features.
///
/// Preview features may change without warning.
///
/// Use comma-separated values or pass multiple times to enable multiple features.
///
/// The following features are available: `python-install-default`, `python-upgrade`,
/// `json-output`, `pylock`, `add-bounds`.
#[arg(
global = true,
long = "preview-features",
env = EnvVars::UV_PREVIEW_FEATURES,
value_delimiter = ',',
hide = true,
alias = "preview-feature",
value_enum,
)]
pub preview_features: Vec<PreviewFeatures>,

/// Avoid discovering a `pyproject.toml` or `uv.toml` file.
///
/// Normally, configuration files are discovered in the current directory,
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-configuration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true, features = ["schemars"] }
uv-platform-tags = { workspace = true }
uv-static = { workspace = true }
uv-warnings = { workspace = true }

bitflags = { workspace = true }
clap = { workspace = true, features = ["derive"], optional = true }
either = { workspace = true }
fs-err = { workspace = true }
Expand Down
248 changes: 227 additions & 21 deletions crates/uv-configuration/src/preview.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,243 @@
use std::fmt::{Display, Formatter};
use std::{
fmt::{Display, Formatter},
str::FromStr,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PreviewMode {
#[default]
Disabled,
Enabled,
}
use thiserror::Error;
use uv_warnings::warn_user_once;

impl PreviewMode {
pub fn is_enabled(&self) -> bool {
matches!(self, Self::Enabled)
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PreviewFeatures: u32 {
const PYTHON_INSTALL_DEFAULT = 1 << 0;
const PYTHON_UPGRADE = 1 << 1;
const JSON_OUTPUT = 1 << 2;
const PYLOCK = 1 << 3;
const ADD_BOUNDS = 1 << 4;
}
}

pub fn is_disabled(&self) -> bool {
matches!(self, Self::Disabled)
impl PreviewFeatures {
/// Returns the string representation of a single preview feature flag.
///
/// Panics if given a combination of flags.
fn flag_as_str(self) -> &'static str {
match self {
Self::PYTHON_INSTALL_DEFAULT => "python-install-default",
Self::PYTHON_UPGRADE => "python-upgrade",
Self::JSON_OUTPUT => "json-output",
Self::PYLOCK => "pylock",
Self::ADD_BOUNDS => "add-bounds",
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
}
}
}

impl From<bool> for PreviewMode {
fn from(version: bool) -> Self {
if version {
PreviewMode::Enabled
impl Display for PreviewFeatures {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_empty() {
write!(f, "none")
} else {
PreviewMode::Disabled
let features: Vec<&str> = self.iter().map(PreviewFeatures::flag_as_str).collect();
write!(f, "{}", features.join(","))
}
}
}

#[derive(Debug, Error, Clone)]
pub enum PreviewFeaturesParseError {
#[error("Empty string in preview features: {0}")]
Empty(String),
}

impl FromStr for PreviewFeatures {
type Err = PreviewFeaturesParseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut flags = PreviewFeatures::empty();

for part in s.split(',') {
let part = part.trim();
if part.is_empty() {
return Err(PreviewFeaturesParseError::Empty(
"Empty string in preview features".to_string(),
));
}

let flag = match part {
"python-install-default" => Self::PYTHON_INSTALL_DEFAULT,
"python-upgrade" => Self::PYTHON_UPGRADE,
"json-output" => Self::JSON_OUTPUT,
"pylock" => Self::PYLOCK,
"add-bounds" => Self::ADD_BOUNDS,
_ => {
warn_user_once!("Unknown preview feature: `{part}`");
continue;
}
};

flags |= flag;
}

Ok(flags)
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Preview {
flags: PreviewFeatures,
}

impl Preview {
pub fn new(flags: PreviewFeatures) -> Self {
Self { flags }
}

pub fn all() -> Self {
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 Display for PreviewMode {
impl Display for Preview {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Disabled => write!(f, "disabled"),
Self::Enabled => write!(f, "enabled"),
if self.flags.is_empty() {
write!(f, "disabled")
} else if self.flags == PreviewFeatures::all() {
write!(f, "enabled")
} else {
write!(f, "{}", self.flags)
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_preview_features_from_str() {
// Test single feature
let features = PreviewFeatures::from_str("python-install-default").unwrap();
assert_eq!(features, PreviewFeatures::PYTHON_INSTALL_DEFAULT);

// Test multiple features
let features = PreviewFeatures::from_str("python-upgrade,json-output").unwrap();
assert!(features.contains(PreviewFeatures::PYTHON_UPGRADE));
assert!(features.contains(PreviewFeatures::JSON_OUTPUT));
assert!(!features.contains(PreviewFeatures::PYLOCK));

// Test with whitespace
let features = PreviewFeatures::from_str("pylock , add-bounds").unwrap();
assert!(features.contains(PreviewFeatures::PYLOCK));
assert!(features.contains(PreviewFeatures::ADD_BOUNDS));

// Test empty string error
assert!(PreviewFeatures::from_str("").is_err());
assert!(PreviewFeatures::from_str("pylock,").is_err());
assert!(PreviewFeatures::from_str(",pylock").is_err());

// Test unknown feature (should be ignored with warning)
let features = PreviewFeatures::from_str("unknown-feature,pylock").unwrap();
assert!(features.contains(PreviewFeatures::PYLOCK));
assert_eq!(features.bits().count_ones(), 1);
}

#[test]
fn test_preview_features_display() {
// Test empty
let features = PreviewFeatures::empty();
assert_eq!(features.to_string(), "none");

// Test single feature
let features = PreviewFeatures::PYTHON_INSTALL_DEFAULT;
assert_eq!(features.to_string(), "python-install-default");

// Test multiple features
let features = PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::JSON_OUTPUT;
assert_eq!(features.to_string(), "python-upgrade,json-output");
}

#[test]
fn test_preview_display() {
// Test disabled
let preview = Preview::default();
assert_eq!(preview.to_string(), "disabled");

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

// Test specific features
let preview = Preview::new(PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::PYLOCK);
assert_eq!(preview.to_string(), "python-upgrade,pylock");
}

#[test]
fn test_preview_from_args() {
// 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 = vec![
PreviewFeatures::PYTHON_UPGRADE,
PreviewFeatures::JSON_OUTPUT,
];
let preview = Preview::from_args(false, false, &features);
assert!(preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE));
assert!(preview.is_enabled(PreviewFeatures::JSON_OUTPUT));
assert!(!preview.is_enabled(PreviewFeatures::PYLOCK));
}

#[test]
fn test_as_str_single_flags() {
assert_eq!(
PreviewFeatures::PYTHON_INSTALL_DEFAULT.flag_as_str(),
"python-install-default"
);
assert_eq!(
PreviewFeatures::PYTHON_UPGRADE.flag_as_str(),
"python-upgrade"
);
assert_eq!(PreviewFeatures::JSON_OUTPUT.flag_as_str(), "json-output");
assert_eq!(PreviewFeatures::PYLOCK.flag_as_str(), "pylock");
assert_eq!(PreviewFeatures::ADD_BOUNDS.flag_as_str(), "add-bounds");
}

#[test]
#[should_panic(expected = "`flag_as_str` can only be used for exactly one feature flag")]
fn test_as_str_multiple_flags_panics() {
let features = PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::JSON_OUTPUT;
let _ = features.flag_as_str();
}
}
4 changes: 2 additions & 2 deletions crates/uv-dev/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use clap::Parser;
use tracing::info;

use uv_cache::{Cache, CacheArgs};
use uv_configuration::{Concurrency, PreviewMode};
use uv_configuration::{Concurrency, Preview};
use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest};

#[derive(Parser)]
Expand All @@ -26,7 +26,7 @@ pub(crate) async fn compile(args: CompileArgs) -> anyhow::Result<()> {
&PythonRequest::default(),
EnvironmentPreference::OnlyVirtual,
&cache,
PreviewMode::Disabled,
Preview::default(),
)?
.into_interpreter();
interpreter.sys_executable().to_path_buf()
Expand Down
Loading
Loading