mirror of https://github.com/astral-sh/uv
297 lines
10 KiB
Rust
297 lines
10 KiB
Rust
use std::{
|
|
fmt::{Display, Formatter},
|
|
str::FromStr,
|
|
};
|
|
|
|
use thiserror::Error;
|
|
use uv_warnings::warn_user_once;
|
|
|
|
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;
|
|
const PACKAGE_CONFLICTS = 1 << 5;
|
|
const EXTRA_BUILD_DEPENDENCIES = 1 << 6;
|
|
const DETECT_MODULE_CONFLICTS = 1 << 7;
|
|
const FORMAT = 1 << 8;
|
|
const NATIVE_AUTH = 1 << 9;
|
|
const S3_ENDPOINT = 1 << 10;
|
|
const CACHE_SIZE = 1 << 11;
|
|
const INIT_PROJECT_FLAG = 1 << 12;
|
|
const WORKSPACE_METADATA = 1 << 13;
|
|
const WORKSPACE_DIR = 1 << 14;
|
|
const WORKSPACE_LIST = 1 << 15;
|
|
const SBOM_EXPORT = 1 << 16;
|
|
const AUTH_HELPER = 1 << 17;
|
|
}
|
|
}
|
|
|
|
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",
|
|
Self::PACKAGE_CONFLICTS => "package-conflicts",
|
|
Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies",
|
|
Self::DETECT_MODULE_CONFLICTS => "detect-module-conflicts",
|
|
Self::FORMAT => "format",
|
|
Self::NATIVE_AUTH => "native-auth",
|
|
Self::S3_ENDPOINT => "s3-endpoint",
|
|
Self::CACHE_SIZE => "cache-size",
|
|
Self::INIT_PROJECT_FLAG => "init-project-flag",
|
|
Self::WORKSPACE_METADATA => "workspace-metadata",
|
|
Self::WORKSPACE_DIR => "workspace-dir",
|
|
Self::WORKSPACE_LIST => "workspace-list",
|
|
Self::SBOM_EXPORT => "sbom-export",
|
|
Self::AUTH_HELPER => "auth-helper",
|
|
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for PreviewFeatures {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
if self.is_empty() {
|
|
write!(f, "none")
|
|
} else {
|
|
let features: Vec<&str> = self.iter().map(Self::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 = Self::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,
|
|
"package-conflicts" => Self::PACKAGE_CONFLICTS,
|
|
"extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES,
|
|
"detect-module-conflicts" => Self::DETECT_MODULE_CONFLICTS,
|
|
"format" => Self::FORMAT,
|
|
"native-auth" => Self::NATIVE_AUTH,
|
|
"s3-endpoint" => Self::S3_ENDPOINT,
|
|
"cache-size" => Self::CACHE_SIZE,
|
|
"init-project-flag" => Self::INIT_PROJECT_FLAG,
|
|
"workspace-metadata" => Self::WORKSPACE_METADATA,
|
|
"workspace-dir" => Self::WORKSPACE_DIR,
|
|
"workspace-list" => Self::WORKSPACE_LIST,
|
|
"sbom-export" => Self::SBOM_EXPORT,
|
|
"auth-helper" => Self::AUTH_HELPER,
|
|
_ => {
|
|
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 Preview {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
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");
|
|
assert_eq!(
|
|
PreviewFeatures::PACKAGE_CONFLICTS.flag_as_str(),
|
|
"package-conflicts"
|
|
);
|
|
assert_eq!(
|
|
PreviewFeatures::EXTRA_BUILD_DEPENDENCIES.flag_as_str(),
|
|
"extra-build-dependencies"
|
|
);
|
|
assert_eq!(
|
|
PreviewFeatures::DETECT_MODULE_CONFLICTS.flag_as_str(),
|
|
"detect-module-conflicts"
|
|
);
|
|
assert_eq!(PreviewFeatures::FORMAT.flag_as_str(), "format");
|
|
assert_eq!(PreviewFeatures::S3_ENDPOINT.flag_as_str(), "s3-endpoint");
|
|
assert_eq!(PreviewFeatures::SBOM_EXPORT.flag_as_str(), "sbom-export");
|
|
}
|
|
|
|
#[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();
|
|
}
|
|
}
|