Skip to content

Commit f4d3d7d

Browse files
committed
Added support for [tool.uv.preview-features] in TOML configs
Preview features are specified as a list of strings in TOML, which parse to PreviewFeatures values. CLI arguments like --preview and --no-preview take precedence over config values. --preview-features combines with config values.
1 parent ce7808d commit f4d3d7d

File tree

11 files changed

+1195
-41
lines changed

11 files changed

+1195
-41
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-preview/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ workspace = true
1919
uv-warnings = { workspace = true }
2020

2121
bitflags = { workspace = true }
22+
schemars = { workspace = true, optional = true }
23+
serde = { workspace = true }
2224
thiserror = { workspace = true }
2325

2426
[dev-dependencies]

crates/uv-preview/src/lib.rs

Lines changed: 94 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(feature = "schemars")]
2+
use std::borrow::Cow;
13
use std::{
24
fmt::{Display, Formatter},
35
str::FromStr,
@@ -56,6 +58,57 @@ impl Display for PreviewFeatures {
5658
}
5759
}
5860

61+
#[cfg(feature = "schemars")]
62+
impl schemars::JsonSchema for PreviewFeatures {
63+
fn schema_name() -> Cow<'static, str> {
64+
Cow::Borrowed("PreviewFeatures")
65+
}
66+
67+
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
68+
let choices: Vec<&str> = Self::all().iter().map(Self::flag_as_str).collect();
69+
schemars::json_schema!({
70+
"type": "string",
71+
"enum": choices,
72+
})
73+
}
74+
}
75+
76+
impl<'de> serde::Deserialize<'de> for PreviewFeatures {
77+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78+
where
79+
D: serde::Deserializer<'de>,
80+
{
81+
struct Visitor;
82+
83+
impl serde::de::Visitor<'_> for Visitor {
84+
type Value = PreviewFeatures;
85+
86+
fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
87+
f.write_str("a string")
88+
}
89+
90+
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
91+
where
92+
E: serde::de::Error,
93+
{
94+
PreviewFeatures::from_str(v).map_err(serde::de::Error::custom)
95+
}
96+
}
97+
98+
deserializer.deserialize_str(Visitor)
99+
}
100+
}
101+
102+
impl serde::Serialize for PreviewFeatures {
103+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
104+
where
105+
S: serde::Serializer,
106+
{
107+
let features: Vec<&str> = self.iter().map(Self::flag_as_str).collect();
108+
features.serialize(serializer)
109+
}
110+
}
111+
59112
#[derive(Debug, Error, Clone)]
60113
pub enum PreviewFeaturesParseError {
61114
#[error("Empty string in preview features: {0}")]
@@ -101,6 +154,28 @@ impl FromStr for PreviewFeatures {
101154
}
102155
}
103156

157+
pub enum PreviewFeaturesMode {
158+
EnableAll,
159+
DisableAll,
160+
Selection(PreviewFeatures),
161+
}
162+
163+
impl PreviewFeaturesMode {
164+
pub fn from_bool(b: bool) -> Self {
165+
if b { Self::EnableAll } else { Self::DisableAll }
166+
}
167+
}
168+
169+
impl<'a, I> From<I> for PreviewFeaturesMode
170+
where
171+
I: Iterator<Item = &'a PreviewFeatures>,
172+
{
173+
fn from(features: I) -> Self {
174+
let flags = features.fold(PreviewFeatures::empty(), |f1, f2| f1 | *f2);
175+
Self::Selection(flags)
176+
}
177+
}
178+
104179
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105180
pub struct Preview {
106181
flags: PreviewFeatures,
@@ -115,33 +190,21 @@ impl Preview {
115190
Self::new(PreviewFeatures::all())
116191
}
117192

118-
pub fn from_args(
119-
preview: bool,
120-
no_preview: bool,
121-
preview_features: &[PreviewFeatures],
122-
) -> Self {
123-
if no_preview {
124-
return Self::default();
125-
}
126-
127-
if preview {
128-
return Self::all();
129-
}
130-
131-
let mut flags = PreviewFeatures::empty();
132-
133-
for features in preview_features {
134-
flags |= *features;
135-
}
136-
137-
Self { flags }
138-
}
139-
140193
pub fn is_enabled(&self, flag: PreviewFeatures) -> bool {
141194
self.flags.contains(flag)
142195
}
143196
}
144197

198+
impl From<PreviewFeaturesMode> for Preview {
199+
fn from(mode: PreviewFeaturesMode) -> Self {
200+
match mode {
201+
PreviewFeaturesMode::EnableAll => Self::all(),
202+
PreviewFeaturesMode::DisableAll => Self::default(),
203+
PreviewFeaturesMode::Selection(flags) => Self { flags },
204+
}
205+
}
206+
}
207+
145208
impl Display for Preview {
146209
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147210
if self.flags.is_empty() {
@@ -219,19 +282,21 @@ mod tests {
219282
#[test]
220283
fn test_preview_from_args() {
221284
// Test no_preview
222-
let preview = Preview::from_args(true, true, &[]);
285+
let preview = Preview::from(PreviewFeaturesMode::DisableAll);
223286
assert_eq!(preview.to_string(), "disabled");
224287

225288
// Test preview (all features)
226-
let preview = Preview::from_args(true, false, &[]);
289+
let preview = Preview::from(PreviewFeaturesMode::EnableAll);
227290
assert_eq!(preview.to_string(), "enabled");
228291

229292
// Test specific features
230-
let features = vec![
231-
PreviewFeatures::PYTHON_UPGRADE,
232-
PreviewFeatures::JSON_OUTPUT,
233-
];
234-
let preview = Preview::from_args(false, false, &features);
293+
let preview = Preview::from(PreviewFeaturesMode::from(
294+
[
295+
PreviewFeatures::PYTHON_UPGRADE,
296+
PreviewFeatures::JSON_OUTPUT,
297+
]
298+
.iter(),
299+
));
235300
assert!(preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE));
236301
assert!(preview.is_enabled(PreviewFeatures::JSON_OUTPUT));
237302
assert!(!preview.is_enabled(PreviewFeatures::PYLOCK));

crates/uv-settings/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ uv-macros = { workspace = true }
2828
uv-normalize = { workspace = true, features = ["schemars"] }
2929
uv-options-metadata = { workspace = true }
3030
uv-pep508 = { workspace = true }
31+
uv-preview = { workspace = true, features = ["schemars"] }
3132
uv-pypi-types = { workspace = true }
3233
uv-python = { workspace = true, features = ["schemars", "clap"] }
3334
uv-redacted = { workspace = true }

crates/uv-settings/src/combine.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use uv_distribution_types::{
1212
PipFindLinks, PipIndex,
1313
};
1414
use uv_install_wheel::LinkMode;
15+
use uv_preview::PreviewFeatures;
1516
use uv_pypi_types::{SchemaConflicts, SupportedEnvironments};
1617
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
1718
use uv_redacted::DisplaySafeUrl;
@@ -100,6 +101,7 @@ impl_combine_or!(PipExtraIndex);
100101
impl_combine_or!(PipFindLinks);
101102
impl_combine_or!(PipIndex);
102103
impl_combine_or!(PrereleaseMode);
104+
impl_combine_or!(PreviewFeatures);
103105
impl_combine_or!(PythonDownloads);
104106
impl_combine_or!(PythonPreference);
105107
impl_combine_or!(PythonVersion);

crates/uv-settings/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ fn warn_uv_toml_masked_fields(options: &Options) {
296296
no_cache,
297297
cache_dir,
298298
preview,
299+
preview_features,
299300
python_preference,
300301
python_downloads,
301302
concurrent_downloads,
@@ -387,6 +388,9 @@ fn warn_uv_toml_masked_fields(options: &Options) {
387388
if preview.is_some() {
388389
masked_fields.push("preview");
389390
}
391+
if preview_features.is_some() {
392+
masked_fields.push("preview-features");
393+
}
390394
if python_preference.is_some() {
391395
masked_fields.push("python-preference");
392396
}

crates/uv-settings/src/settings.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use uv_install_wheel::LinkMode;
1515
use uv_macros::{CombineOptions, OptionsMetadata};
1616
use uv_normalize::{ExtraName, PackageName, PipGroupName};
1717
use uv_pep508::Requirement;
18+
use uv_preview::PreviewFeatures;
1819
use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
1920
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
2021
use uv_redacted::DisplaySafeUrl;
@@ -245,7 +246,7 @@ pub struct GlobalOptions {
245246
"#
246247
)]
247248
pub cache_dir: Option<PathBuf>,
248-
/// Whether to enable experimental, preview features.
249+
/// Whether to enable all experimental, preview features.
249250
#[option(
250251
default = "false",
251252
value_type = "bool",
@@ -254,6 +255,15 @@ pub struct GlobalOptions {
254255
"#
255256
)]
256257
pub preview: Option<bool>,
258+
/// Whether to enable specific experimental, preview features.
259+
#[option(
260+
default = "[]",
261+
value_type = "list[str]",
262+
example = r#"
263+
preview-features = ["python-upgrade"]
264+
"#
265+
)]
266+
pub preview_features: Option<Vec<PreviewFeatures>>,
257267
/// Whether to prefer using Python installations that are already present on the system, or
258268
/// those that are downloaded and installed by uv.
259269
#[option(
@@ -2047,6 +2057,7 @@ pub struct OptionsWire {
20472057
no_cache: Option<bool>,
20482058
cache_dir: Option<PathBuf>,
20492059
preview: Option<bool>,
2060+
preview_features: Option<Vec<PreviewFeatures>>,
20502061
python_preference: Option<PythonPreference>,
20512062
python_downloads: Option<PythonDownloads>,
20522063
concurrent_downloads: Option<NonZeroUsize>,
@@ -2140,6 +2151,7 @@ impl From<OptionsWire> for Options {
21402151
no_cache,
21412152
cache_dir,
21422153
preview,
2154+
preview_features,
21432155
python_preference,
21442156
python_downloads,
21452157
python_install_mirror,
@@ -2210,6 +2222,7 @@ impl From<OptionsWire> for Options {
22102222
no_cache,
22112223
cache_dir,
22122224
preview,
2225+
preview_features,
22132226
python_preference,
22142227
python_downloads,
22152228
concurrent_downloads,

crates/uv/src/settings.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use uv_distribution_types::{
3636
use uv_install_wheel::LinkMode;
3737
use uv_normalize::{ExtraName, PackageName, PipGroupName};
3838
use uv_pep508::{MarkerTree, RequirementOrigin};
39-
use uv_preview::Preview;
39+
use uv_preview::{Preview, PreviewFeaturesMode};
4040
use uv_pypi_types::SupportedEnvironments;
4141
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
4242
use uv_redacted::DisplaySafeUrl;
@@ -137,13 +137,7 @@ impl GlobalSettings {
137137
.unwrap_or_else(Concurrency::threads),
138138
},
139139
show_settings: args.show_settings,
140-
preview: Preview::from_args(
141-
flag(args.preview, args.no_preview, "preview")
142-
.combine(workspace.and_then(|workspace| workspace.globals.preview))
143-
.unwrap_or(false),
144-
args.no_preview,
145-
&args.preview_features,
146-
),
140+
preview: Preview::from(resolve_preview_settings(args, workspace)),
147141
python_preference,
148142
python_downloads: flag(
149143
args.allow_python_downloads,
@@ -177,6 +171,28 @@ fn resolve_python_preference(
177171
}
178172
}
179173

174+
fn resolve_preview_settings(
175+
args: &GlobalArgs,
176+
workspace: Option<&FilesystemOptions>,
177+
) -> PreviewFeaturesMode {
178+
if let Some(preview) = flag(args.preview, args.no_preview, "preview") {
179+
return PreviewFeaturesMode::from_bool(preview);
180+
}
181+
match workspace.and_then(|workspace| workspace.globals.preview) {
182+
Some(true) => PreviewFeaturesMode::EnableAll,
183+
// Disable workspace `preview-features` but keep any CLI `--preview-features`
184+
Some(false) => PreviewFeaturesMode::from(args.preview_features.iter()),
185+
None => {
186+
let features = args.preview_features.iter().chain(
187+
workspace
188+
.and_then(|workspace| workspace.globals.preview_features.as_deref())
189+
.unwrap_or_default(),
190+
);
191+
PreviewFeaturesMode::from(features)
192+
}
193+
}
194+
}
195+
180196
/// The resolved network settings to use for any invocation of the CLI.
181197
#[derive(Debug, Clone)]
182198
pub(crate) struct NetworkSettings {

0 commit comments

Comments
 (0)