mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Allow configuring preview features in uv.toml and pyproject.toml (#18437)
## Summary This is a continuation of the excellent work by @j-helland in #16452 and #17202 and closes #15767. This PR adds `preview-features` to `uv.toml` and `pyproject.toml`. This field can be set to a boolean or to a list of features. It is intended to supersede the `preview` setting itself, and conflicts with it. There's a little bit of complexity required to ensure that setting either `preview` or `preview-features` is combined at the right point, but it's also necessary to leave them split to accurately warn when a `uv.toml` masks a `pyproject.toml` in the same directory. Additionally there's complexity involved in making things work with `deny_unknown_fields` and `flatten` (they're incompatible so require manual flattening) and also this improves error messages somewhat. Also (supersedes) closes #16452 and (supersedes) closes #17202. ## Test Plan Tests taken from the original PR with some alterations and a bunch of additional tests. --------- Co-authored-by: j-helland <jonathan.w.helland@gmail.com> Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
committed by
GitHub
parent
6736974a73
commit
ea707737bb
Generated
+4
@@ -6839,6 +6839,8 @@ version = "0.0.54"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"itertools 0.14.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"uv-preview",
|
||||
"uv-warnings",
|
||||
@@ -7162,6 +7164,7 @@ dependencies = [
|
||||
"fs-err",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde-untagged",
|
||||
"textwrap",
|
||||
"thiserror",
|
||||
"toml",
|
||||
@@ -7180,6 +7183,7 @@ dependencies = [
|
||||
"uv-options-metadata",
|
||||
"uv-pep440",
|
||||
"uv-pep508",
|
||||
"uv-preview",
|
||||
"uv-pypi-types",
|
||||
"uv-python",
|
||||
"uv-redacted",
|
||||
|
||||
@@ -23,6 +23,8 @@ uv-warnings = { workspace = true }
|
||||
|
||||
enumflags2 = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -30,4 +32,5 @@ uv-preview = { path = ".", features = ["testing"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
schemars = ["dep:schemars"]
|
||||
testing = []
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::borrow::Cow;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter},
|
||||
@@ -384,6 +385,43 @@ impl FromStr for MaybePreviewFeature {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for MaybePreviewFeature {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let name: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?;
|
||||
Self::from_str(&name).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for MaybePreviewFeature {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("PreviewFeature")
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
// Advertise canonical names for editor completions, while accepting any nonempty name to
|
||||
// match the forwards-compatible runtime parsing behavior.
|
||||
let choices: Vec<&str> = BitFlags::<PreviewFeature>::all()
|
||||
.iter()
|
||||
.map(PreviewFeature::as_str)
|
||||
.collect();
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": choices,
|
||||
},
|
||||
{
|
||||
"pattern": "\\S",
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Preview {
|
||||
flags: BitFlags<PreviewFeature>,
|
||||
@@ -409,23 +447,6 @@ impl Preview {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve preview arguments, warning and ignoring unknown feature names.
|
||||
pub fn from_args(
|
||||
preview: bool,
|
||||
no_preview: bool,
|
||||
preview_features: &[MaybePreviewFeature],
|
||||
) -> Self {
|
||||
if no_preview {
|
||||
return Self::default();
|
||||
}
|
||||
|
||||
if preview {
|
||||
return Self::all();
|
||||
}
|
||||
|
||||
Self::from_feature_names(preview_features)
|
||||
}
|
||||
|
||||
/// Check if a single feature is enabled.
|
||||
pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
|
||||
self.flags.contains(flag)
|
||||
@@ -441,7 +462,8 @@ impl Preview {
|
||||
!self.flags.is_empty()
|
||||
}
|
||||
|
||||
fn from_feature_names<'a>(
|
||||
/// Resolve preview feature names, warning and ignoring unknown names.
|
||||
pub fn from_feature_names<'a>(
|
||||
feature_names: impl IntoIterator<Item = &'a MaybePreviewFeature>,
|
||||
) -> Self {
|
||||
let mut flags = BitFlags::empty();
|
||||
@@ -547,34 +569,6 @@ mod tests {
|
||||
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, &[]);
|
||||
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 = ["python-upgrade", "json-output"].map(|name| name.parse().unwrap());
|
||||
let preview = Preview::from_args(false, false, &features);
|
||||
assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
|
||||
assert!(preview.is_enabled(PreviewFeature::JsonOutput));
|
||||
assert!(!preview.is_enabled(PreviewFeature::Pylock));
|
||||
|
||||
// Test unknown features
|
||||
let features = ["unknown-feature", "pylock"].map(|name| name.parse().unwrap());
|
||||
let preview = Preview::from_args(false, false, &features);
|
||||
assert!(preview.is_enabled(PreviewFeature::Pylock));
|
||||
assert_eq!(preview.flags.bits().count_ones(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preview_feature_as_str() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -30,6 +30,7 @@ uv-normalize = { workspace = true }
|
||||
uv-options-metadata = { workspace = true }
|
||||
uv-pep440 = { workspace = true }
|
||||
uv-pep508 = { workspace = true }
|
||||
uv-preview = { workspace = true }
|
||||
uv-pypi-types = { workspace = true }
|
||||
uv-python = { workspace = true, features = ["clap"] }
|
||||
uv-redacted = { workspace = true }
|
||||
@@ -44,6 +45,7 @@ clap = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde-untagged = { workspace = true }
|
||||
textwrap = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
@@ -51,7 +53,7 @@ tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = ["uv-options-metadata", "clap"]
|
||||
ignored = ["clap"]
|
||||
|
||||
[features]
|
||||
schemars = [
|
||||
@@ -61,6 +63,7 @@ schemars = [
|
||||
"uv-distribution-types/schemars",
|
||||
"uv-install-wheel/schemars",
|
||||
"uv-normalize/schemars",
|
||||
"uv-preview/schemars",
|
||||
"uv-python/schemars",
|
||||
"uv-resolver/schemars",
|
||||
"uv-torch/schemars",
|
||||
|
||||
@@ -23,7 +23,7 @@ use uv_torch::TorchMode;
|
||||
use uv_workspace::pyproject::ExtraBuildDependencies;
|
||||
use uv_workspace::pyproject_mut::AddBoundsKind;
|
||||
|
||||
use crate::{AuditOptions, FilesystemOptions, Options, PipOptions};
|
||||
use crate::{AuditOptions, FilesystemOptions, Options, PipOptions, PreviewOption};
|
||||
|
||||
pub trait Combine {
|
||||
/// Combine two values, preferring the values in `self`.
|
||||
@@ -109,6 +109,7 @@ impl_combine_or!(PipExtraIndex);
|
||||
impl_combine_or!(PipFindLinks);
|
||||
impl_combine_or!(PipIndex);
|
||||
impl_combine_or!(PrereleaseMode);
|
||||
impl_combine_or!(PreviewOption);
|
||||
impl_combine_or!(ProxyUrl);
|
||||
impl_combine_or!(PythonDownloads);
|
||||
impl_combine_or!(PythonPreference);
|
||||
|
||||
@@ -481,8 +481,10 @@ fn warn_uv_toml_masked_fields(options: &Options) {
|
||||
if cache_dir.is_some() {
|
||||
masked_fields.push("cache-dir");
|
||||
}
|
||||
if preview.is_some() {
|
||||
masked_fields.push("preview");
|
||||
match preview {
|
||||
Some(PreviewOption::Preview(_)) => masked_fields.push("preview"),
|
||||
Some(PreviewOption::PreviewFeatures(_)) => masked_fields.push("preview-features"),
|
||||
None => (),
|
||||
}
|
||||
if python_preference.is_some() {
|
||||
masked_fields.push("python-preference");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(feature = "schemars")]
|
||||
use std::borrow::Cow;
|
||||
use std::{fmt::Debug, num::NonZeroUsize, path::Path, path::PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -15,6 +17,7 @@ use uv_install_wheel::LinkMode;
|
||||
use uv_macros::{CombineOptions, OptionsMetadata};
|
||||
use uv_normalize::{ExtraName, PackageName, PipGroupName};
|
||||
use uv_pep508::Requirement;
|
||||
use uv_preview::{MaybePreviewFeature, Preview};
|
||||
use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
|
||||
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
@@ -69,9 +72,9 @@ pub(crate) struct UvRequiredVersionToml {
|
||||
/// A `[tool.uv]` section.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
|
||||
#[serde(from = "OptionsWire", rename_all = "kebab-case")]
|
||||
#[serde(try_from = "OptionsWire", rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schemars", schemars(!from))]
|
||||
#[cfg_attr(feature = "schemars", schemars(!try_from))]
|
||||
pub struct Options {
|
||||
#[serde(flatten)]
|
||||
pub globals: GlobalOptions,
|
||||
@@ -246,8 +249,9 @@ impl Options {
|
||||
|
||||
/// Global settings, relevant to all invocations.
|
||||
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(try_from = "GlobalOptionsWire", rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schemars", schemars(!try_from))]
|
||||
pub struct GlobalOptions {
|
||||
/// Enforce a requirement on the version of uv.
|
||||
///
|
||||
@@ -324,15 +328,11 @@ pub struct GlobalOptions {
|
||||
"#
|
||||
)]
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
/// Whether to enable experimental, preview features.
|
||||
#[option(
|
||||
default = "false",
|
||||
value_type = "bool",
|
||||
example = r#"
|
||||
preview = true
|
||||
"#
|
||||
)]
|
||||
pub preview: Option<bool>,
|
||||
|
||||
/// The user's preview configuration.
|
||||
#[serde(flatten)]
|
||||
pub preview: Option<PreviewOption>,
|
||||
|
||||
/// Whether to prefer using Python installations that are already present on the system, or
|
||||
/// those that are downloaded and installed by uv.
|
||||
#[option(
|
||||
@@ -435,6 +435,78 @@ pub struct GlobalOptions {
|
||||
pub allow_insecure_host: Option<Vec<TrustedHost>>,
|
||||
}
|
||||
|
||||
/// Like [`GlobalOptions`], but with any `#[serde(flatten)]` fields inlined.
|
||||
/// This improves line/column information in error messages.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct GlobalOptionsWire {
|
||||
required_version: Option<RequiredVersion>,
|
||||
system_certs: Option<bool>,
|
||||
native_tls: Option<bool>,
|
||||
offline: Option<bool>,
|
||||
no_cache: Option<bool>,
|
||||
cache_dir: Option<PathBuf>,
|
||||
|
||||
preview: Option<bool>,
|
||||
preview_features: Option<PreviewFeaturesOption>,
|
||||
|
||||
python_preference: Option<PythonPreference>,
|
||||
python_downloads: Option<PythonDownloads>,
|
||||
concurrent_downloads: Option<NonZeroUsize>,
|
||||
concurrent_builds: Option<NonZeroUsize>,
|
||||
concurrent_installs: Option<NonZeroUsize>,
|
||||
http_proxy: Option<ProxyUrl>,
|
||||
https_proxy: Option<ProxyUrl>,
|
||||
no_proxy: Option<Vec<String>>,
|
||||
allow_insecure_host: Option<Vec<TrustedHost>>,
|
||||
}
|
||||
|
||||
impl TryFrom<GlobalOptionsWire> for GlobalOptions {
|
||||
type Error = &'static str;
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn try_from(value: GlobalOptionsWire) -> Result<Self, Self::Error> {
|
||||
let GlobalOptionsWire {
|
||||
required_version,
|
||||
system_certs,
|
||||
native_tls,
|
||||
offline,
|
||||
no_cache,
|
||||
cache_dir,
|
||||
preview,
|
||||
preview_features,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
concurrent_downloads,
|
||||
concurrent_builds,
|
||||
concurrent_installs,
|
||||
http_proxy,
|
||||
https_proxy,
|
||||
no_proxy,
|
||||
allow_insecure_host,
|
||||
} = value;
|
||||
|
||||
Ok(Self {
|
||||
required_version,
|
||||
system_certs,
|
||||
native_tls,
|
||||
offline,
|
||||
no_cache,
|
||||
cache_dir,
|
||||
preview: PreviewOption::try_from(preview, preview_features)?,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
concurrent_downloads,
|
||||
concurrent_builds,
|
||||
concurrent_installs,
|
||||
http_proxy,
|
||||
https_proxy,
|
||||
no_proxy,
|
||||
allow_insecure_host,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings relevant to all installer operations.
|
||||
#[derive(Debug, Clone, Default, CombineOptions)]
|
||||
pub struct InstallerOptions {
|
||||
@@ -2381,6 +2453,7 @@ struct OptionsWire {
|
||||
no_cache: Option<bool>,
|
||||
cache_dir: Option<PathBuf>,
|
||||
preview: Option<bool>,
|
||||
preview_features: Option<PreviewFeaturesOption>,
|
||||
python_preference: Option<PythonPreference>,
|
||||
python_downloads: Option<PythonDownloads>,
|
||||
concurrent_downloads: Option<NonZeroUsize>,
|
||||
@@ -2472,9 +2545,11 @@ struct OptionsWire {
|
||||
build_backend: Option<serde::de::IgnoredAny>,
|
||||
}
|
||||
|
||||
impl From<OptionsWire> for Options {
|
||||
impl TryFrom<OptionsWire> for Options {
|
||||
type Error = &'static str;
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn from(value: OptionsWire) -> Self {
|
||||
fn try_from(value: OptionsWire) -> Result<Self, Self::Error> {
|
||||
let OptionsWire {
|
||||
required_version,
|
||||
system_certs,
|
||||
@@ -2483,6 +2558,7 @@ impl From<OptionsWire> for Options {
|
||||
no_cache,
|
||||
cache_dir,
|
||||
preview,
|
||||
preview_features,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
python_install_mirror,
|
||||
@@ -2552,7 +2628,7 @@ impl From<OptionsWire> for Options {
|
||||
build_backend,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
globals: GlobalOptions {
|
||||
required_version,
|
||||
system_certs,
|
||||
@@ -2560,7 +2636,7 @@ impl From<OptionsWire> for Options {
|
||||
offline,
|
||||
no_cache,
|
||||
cache_dir,
|
||||
preview,
|
||||
preview: PreviewOption::try_from(preview, preview_features)?,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
concurrent_downloads,
|
||||
@@ -2635,7 +2711,7 @@ impl From<OptionsWire> for Options {
|
||||
dependency_groups,
|
||||
managed,
|
||||
package,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2765,3 +2841,116 @@ impl From<&crate::EnvironmentOptions> for MalwareCheckSettings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the `preview-features` configuration option.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schemars", schemars(untagged))]
|
||||
pub enum PreviewFeaturesOption {
|
||||
Toggle(bool),
|
||||
Features(Vec<MaybePreviewFeature>),
|
||||
}
|
||||
|
||||
// A derived `#[serde(untagged)]` implementation collapses detailed type and element errors into
|
||||
// "data did not match any variant", so use a type-directed visitor to preserve useful diagnostics.
|
||||
impl<'de> Deserialize<'de> for PreviewFeaturesOption {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
serde_untagged::UntaggedEnumVisitor::new()
|
||||
.expecting("a boolean or a list of preview feature names")
|
||||
.bool(|value| Ok(Self::Toggle(value)))
|
||||
.seq(|sequence| sequence.deserialize().map(Self::Features))
|
||||
.deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
dead_code,
|
||||
reason = "Fields are only used by the OptionsMetadata and JsonSchema derives"
|
||||
)]
|
||||
#[derive(OptionsMetadata)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schemars", schemars(rename_all = "kebab-case"))]
|
||||
struct PreviewOptionsDefinition {
|
||||
// This legacy setting remains supported and included in the JSON schema, but is omitted from
|
||||
// option metadata so the generated settings reference documents only `preview-features`.
|
||||
/// Whether to enable all experimental, preview features.
|
||||
///
|
||||
/// Use `preview-features` instead.
|
||||
#[deprecated(note = "use `preview-features` instead")]
|
||||
preview: Option<bool>,
|
||||
/// Whether to enable specific or all experimental preview features.
|
||||
///
|
||||
/// Unknown feature names are ignored with a warning.
|
||||
#[option(
|
||||
default = "false",
|
||||
value_type = "bool | list[str]",
|
||||
example = r#"
|
||||
preview-features = true
|
||||
# or
|
||||
preview-features = ["python-upgrade"]
|
||||
"#
|
||||
)]
|
||||
preview_features: Option<PreviewFeaturesOption>,
|
||||
}
|
||||
|
||||
/// Represents the user's preview configuration from either `preview` or `preview-features`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PreviewOption {
|
||||
/// Whether to enable all experimental, preview features.
|
||||
Preview(bool),
|
||||
/// Whether to enable specific or all experimental preview features.
|
||||
PreviewFeatures(PreviewFeaturesOption),
|
||||
}
|
||||
|
||||
impl uv_options_metadata::OptionsMetadata for PreviewOption {
|
||||
fn record(visit: &mut dyn uv_options_metadata::Visit) {
|
||||
<PreviewOptionsDefinition as uv_options_metadata::OptionsMetadata>::record(visit);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for PreviewOption {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("PreviewOption")
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let mut schema = <PreviewOptionsDefinition as schemars::JsonSchema>::json_schema(generator);
|
||||
schema.insert(
|
||||
"not".to_string(),
|
||||
schemars::json_schema!({
|
||||
"required": ["preview", "preview-features"],
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
schema
|
||||
}
|
||||
}
|
||||
|
||||
impl PreviewOption {
|
||||
fn try_from(
|
||||
preview: Option<bool>,
|
||||
preview_features: Option<PreviewFeaturesOption>,
|
||||
) -> Result<Option<Self>, &'static str> {
|
||||
match (preview, preview_features) {
|
||||
(Some(_), Some(_)) => Err("cannot specify both `preview` and `preview-features`"),
|
||||
(Some(b), None) => Ok(Some(Self::Preview(b))),
|
||||
(None, Some(features)) => Ok(Some(Self::PreviewFeatures(features))),
|
||||
(None, None) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the preview configuration, warning and ignoring unknown feature names.
|
||||
pub fn resolve(&self) -> Preview {
|
||||
use PreviewFeaturesOption::{Features, Toggle};
|
||||
|
||||
match self {
|
||||
Self::Preview(false) | Self::PreviewFeatures(Toggle(false)) => Preview::default(),
|
||||
Self::Preview(true) | Self::PreviewFeatures(Toggle(true)) => Preview::all(),
|
||||
Self::PreviewFeatures(Features(features)) => Preview::from_feature_names(features),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ use uv_fs::{CWD, Simplified, normalize_path};
|
||||
#[cfg(feature = "self-update")]
|
||||
use uv_pep440::release_specifiers_to_ranges;
|
||||
use uv_pep508::VersionOrUrl;
|
||||
use uv_preview::{Preview, PreviewFeature};
|
||||
use uv_preview::PreviewFeature;
|
||||
use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl};
|
||||
use uv_python::PythonRequest;
|
||||
use uv_requirements::{GroupsSpecification, RequirementsSource};
|
||||
@@ -127,11 +127,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||
let environment = EnvironmentOptions::new()?;
|
||||
|
||||
// Resolve preview flags before config discovery for decisions that affect the discovery root.
|
||||
let early_preview = Preview::from_args(
|
||||
settings::resolve_preview(&cli.top_level.global_args, None, &environment),
|
||||
cli.top_level.global_args.no_preview,
|
||||
&cli.top_level.global_args.preview_features,
|
||||
);
|
||||
let early_preview = settings::resolve_preview(&cli.top_level.global_args, None, &environment);
|
||||
|
||||
// Make the early preview flags globally available.
|
||||
uv_preview::set(early_preview)?;
|
||||
|
||||
+37
-22
@@ -57,8 +57,8 @@ use uv_resolver::{
|
||||
};
|
||||
use uv_settings::{
|
||||
Combine, EnvironmentOptions, FilesystemOptions, MalwareCheckSettings, Options, PipOptions,
|
||||
PublishOptions, PythonInstallMirrors, ResolverInstallerOptions, ResolverInstallerSchema,
|
||||
ResolverOptions,
|
||||
PreviewFeaturesOption, PreviewOption, PublishOptions, PythonInstallMirrors,
|
||||
ResolverInstallerOptions, ResolverInstallerSchema, ResolverOptions,
|
||||
};
|
||||
use uv_static::EnvVars;
|
||||
use uv_torch::{AmdGpuArchitecture, TorchMode};
|
||||
@@ -129,11 +129,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,
|
||||
@@ -228,22 +224,41 @@ pub(crate) 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 {
|
||||
// Explicit `--preview` and `--no-preview` flags take priority.
|
||||
if let Some(enabled) = flag(args.preview, args.no_preview, "preview") {
|
||||
return if enabled {
|
||||
Preview::all()
|
||||
} else {
|
||||
Preview::default()
|
||||
};
|
||||
}
|
||||
|
||||
// `UV_PREVIEW=true` enables all preview features.
|
||||
if environment.preview.value == Some(true) {
|
||||
return Preview::all();
|
||||
}
|
||||
|
||||
let configured = workspace.and_then(|workspace| workspace.globals.preview.as_ref());
|
||||
|
||||
// Boolean enable-all configuration takes priority.
|
||||
if matches!(
|
||||
configured,
|
||||
Some(
|
||||
PreviewOption::Preview(true)
|
||||
| PreviewOption::PreviewFeatures(PreviewFeaturesOption::Toggle(true))
|
||||
)
|
||||
) {
|
||||
return Preview::all();
|
||||
}
|
||||
|
||||
// Explicit preview feature names take priority over configured feature names.
|
||||
if !args.preview_features.is_empty() {
|
||||
return Preview::from_feature_names(&args.preview_features);
|
||||
}
|
||||
|
||||
// Fall back to workspace configuration.
|
||||
configured.map(PreviewOption::resolve).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The resolved network settings to use for any invocation of the CLI.
|
||||
|
||||
@@ -265,7 +265,7 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> {
|
||||
|
|
||||
2 | unknown = "field"
|
||||
| ^^^^^^^
|
||||
unknown field `unknown`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
|
||||
unknown field `unknown`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `preview-features`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
|
||||
|
||||
Resolved in [TIME]
|
||||
Checked in [TIME]
|
||||
|
||||
@@ -2119,6 +2119,95 @@ fn resolve_both_special_fields() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read preview settings from both a `uv.toml` and `pyproject.toml` file in the current directory.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
ignore = "Configuration tests are not yet supported on Windows"
|
||||
)]
|
||||
fn resolve_both_preview() -> anyhow::Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
let uv_config = context.temp_dir.child("uv.toml");
|
||||
let pyproject = context.temp_dir.child("pyproject.toml");
|
||||
|
||||
let baseline = capture_uv_snapshot!(
|
||||
context.filters(),
|
||||
add_shared_args(context.version()).arg("--show-settings")
|
||||
);
|
||||
|
||||
pyproject.write_str(indoc::indoc! {r"
|
||||
[tool.uv]
|
||||
preview = true
|
||||
"})?;
|
||||
uv_config.write_str(r#"preview-features = ["pylock"]"#)?;
|
||||
|
||||
// The warning should name the ignored `preview` setting.
|
||||
// Compare against the baseline.
|
||||
let preview_masked = diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&baseline,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@"
|
||||
...
|
||||
},
|
||||
show_settings: true,
|
||||
preview: Preview {
|
||||
- flags: [],
|
||||
+ flags: [
|
||||
+ Pylock,
|
||||
+ ],
|
||||
},
|
||||
python_preference: Managed,
|
||||
python_downloads: Automatic,
|
||||
...
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
+warning: Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`. The following fields from `[tool.uv]` will be ignored in favor of the `uv.toml` file:
|
||||
+- preview
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
pyproject.write_str(indoc::indoc! {r#"
|
||||
[tool.uv]
|
||||
preview-features = ["unknown-preview-feature"]
|
||||
"#})?;
|
||||
uv_config.write_str("preview = false")?;
|
||||
|
||||
// The warning should name the ignored `preview-features` setting without warning about its
|
||||
// unknown feature name.
|
||||
// Compare against the inverse spelling combination.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&preview_masked,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@"
|
||||
...
|
||||
},
|
||||
show_settings: true,
|
||||
preview: Preview {
|
||||
- flags: [
|
||||
- Pylock,
|
||||
- ],
|
||||
+ flags: [],
|
||||
},
|
||||
python_preference: Managed,
|
||||
python_downloads: Automatic,
|
||||
...
|
||||
|
||||
----- stderr -----
|
||||
warning: Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`. The following fields from `[tool.uv]` will be ignored in favor of the `uv.toml` file:
|
||||
-- preview
|
||||
+- preview-features
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that errors when parsing `conflicts` are reported.
|
||||
#[test]
|
||||
fn invalid_conflicts() -> anyhow::Result<()> {
|
||||
@@ -2404,7 +2493,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
||||
|
|
||||
1 | [project]
|
||||
| ^^^^^^^
|
||||
unknown field `project`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
|
||||
unknown field `project`, expected one of `required-version`, `system-certs`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `preview-features`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `http-proxy`, `https-proxy`, `no-proxy`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `config-settings-package`, `no-build-isolation`, `no-build-isolation-package`, `extra-build-dependencies`, `extra-build-variables`, `exclude-newer`, `exclude-newer-package`, `link-mode`, `compile-bytecode`, `no-sources`, `no-sources-package`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `torch-backend`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `audit`, `pip`, `cache-keys`, `override-dependencies`, `exclude-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dependency-groups`, `dev-dependencies`, `build-backend`
|
||||
"
|
||||
);
|
||||
|
||||
@@ -3088,6 +3177,727 @@ fn preview_features() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Test preview precedence across configuration, environment, and CLI settings.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
ignore = "Configuration tests are not yet supported on Windows"
|
||||
)]
|
||||
fn preview_precedence() -> anyhow::Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
let show_settings = || {
|
||||
let mut cmd = context.version();
|
||||
cmd.arg("--show-settings");
|
||||
add_shared_args(cmd)
|
||||
};
|
||||
|
||||
let disabled = capture_uv_snapshot!(context.filters(), show_settings());
|
||||
|
||||
let enabled = capture_uv_snapshot!(context.filters(), show_settings().arg("--preview"));
|
||||
|
||||
let user_config_dir = context.user_config_dir.child("uv");
|
||||
user_config_dir.create_dir_all()?;
|
||||
let user_config = user_config_dir.child("uv.toml");
|
||||
user_config.write_str("preview = true")?;
|
||||
|
||||
// `preview = true` in user `uv.toml` should be the same as passing `--preview`.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&enabled,
|
||||
show_settings(),
|
||||
@""
|
||||
);
|
||||
|
||||
let project_config = context.temp_dir.child("pyproject.toml");
|
||||
project_config.write_str(indoc::indoc! {r"
|
||||
[tool.uv]
|
||||
preview = false
|
||||
"})?;
|
||||
|
||||
// `preview = false` in `pyproject.toml` should mask user `uv.toml`.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
show_settings(),
|
||||
@""
|
||||
);
|
||||
|
||||
// `--preview-features` should mask `preview = false` in a config file.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
show_settings().arg("--preview-features").arg("pylock"),
|
||||
@"
|
||||
...
|
||||
},
|
||||
show_settings: true,
|
||||
preview: Preview {
|
||||
- flags: [],
|
||||
+ flags: [
|
||||
+ Pylock,
|
||||
+ ],
|
||||
},
|
||||
python_preference: Managed,
|
||||
python_downloads: Automatic,
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
user_config.write_str("preview = false")?;
|
||||
|
||||
project_config.write_str(indoc::indoc! {r"
|
||||
[tool.uv]
|
||||
preview = true
|
||||
"})?;
|
||||
|
||||
// `preview = true` in a config file should mask `--preview-features`.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&enabled,
|
||||
show_settings().arg("--preview-features").arg("pylock"),
|
||||
@""
|
||||
);
|
||||
|
||||
// `UV_PREVIEW=false` should fall through to configuration.
|
||||
// Compare against `--preview`.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&enabled,
|
||||
show_settings().env(EnvVars::UV_PREVIEW, "0"),
|
||||
@""
|
||||
);
|
||||
|
||||
project_config.write_str("")?;
|
||||
user_config.write_str("")?;
|
||||
|
||||
// `UV_PREVIEW=true` should override any explicit CLI feature list.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&enabled,
|
||||
show_settings()
|
||||
.arg("--preview-features")
|
||||
.arg("pylock")
|
||||
.env(EnvVars::UV_PREVIEW, "1"),
|
||||
@""
|
||||
);
|
||||
|
||||
// `--no-preview` should override `UV_PREVIEW=true`.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
show_settings()
|
||||
.arg("--no-preview")
|
||||
.env(EnvVars::UV_PREVIEW, "1"),
|
||||
@""
|
||||
);
|
||||
|
||||
user_config.write_str("preview = true")?;
|
||||
project_config.write_str(indoc::indoc! {r#"
|
||||
[tool.uv]
|
||||
preview-features = ["format-command"]
|
||||
"#})?;
|
||||
|
||||
// The project setting atomically overrides the differently-spelled user setting.
|
||||
// Compare against the disabled baseline.
|
||||
let project = diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
show_settings(),
|
||||
@"
|
||||
...
|
||||
},
|
||||
show_settings: true,
|
||||
preview: Preview {
|
||||
- flags: [],
|
||||
+ flags: [
|
||||
+ Format,
|
||||
+ ],
|
||||
},
|
||||
python_preference: Managed,
|
||||
python_downloads: Automatic,
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
user_config.write_str(r#"preview-features = ["unknown-preview-feature"]"#)?;
|
||||
|
||||
// An unknown lower-priority setting is masked without warning.
|
||||
// Compare against the recognized project setting.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&project,
|
||||
show_settings(),
|
||||
@""
|
||||
);
|
||||
|
||||
user_config.write_str("preview = true")?;
|
||||
project_config.write_str(indoc::indoc! {r"
|
||||
[tool.uv]
|
||||
preview-features = false
|
||||
"})?;
|
||||
|
||||
// An explicit disabled project setting atomically overrides the enabled user setting.
|
||||
// Compare against the disabled baseline.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
show_settings(),
|
||||
@""
|
||||
);
|
||||
|
||||
project_config.write_str(indoc::indoc! {r#"
|
||||
[tool.uv]
|
||||
preview-features = ["format-command"]
|
||||
"#})?;
|
||||
|
||||
// An explicit CLI feature list should override the project feature list.
|
||||
// Compare against the project feature list.
|
||||
let cli_features = diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&project,
|
||||
show_settings().arg("--preview-features").arg("pylock"),
|
||||
@"
|
||||
...
|
||||
show_settings: true,
|
||||
preview: Preview {
|
||||
flags: [
|
||||
- Format,
|
||||
+ Pylock,
|
||||
],
|
||||
},
|
||||
python_preference: Managed,
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
project_config.write_str(indoc::indoc! {r"
|
||||
[tool.uv]
|
||||
preview-features = false
|
||||
"})?;
|
||||
|
||||
// An explicit CLI feature list should override the disabled project setting.
|
||||
// Compare against the explicit CLI feature list.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&cli_features,
|
||||
show_settings().arg("--preview-features").arg("pylock"),
|
||||
@""
|
||||
);
|
||||
|
||||
project_config.write_str(indoc::indoc! {r#"
|
||||
[tool.uv]
|
||||
preview-features = ["unknown-preview-feature"]
|
||||
"#})?;
|
||||
|
||||
// An unknown-only project setting atomically overrides the enabled user setting.
|
||||
// Compare against the disabled baseline.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
show_settings(),
|
||||
@"
|
||||
...
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
+warning: Unknown preview feature: `unknown-preview-feature`
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
// An explicit CLI setting masks the unknown project setting without warning.
|
||||
// Compare against the explicit CLI feature list.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&cli_features,
|
||||
show_settings().arg("--preview-features").arg("pylock"),
|
||||
@""
|
||||
);
|
||||
|
||||
// `--preview` masks the unknown project setting without warning.
|
||||
// Compare against `--preview`.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&enabled,
|
||||
show_settings().arg("--preview"),
|
||||
@""
|
||||
);
|
||||
|
||||
project_config.write_str(indoc::indoc! {r"
|
||||
[tool.uv]
|
||||
preview-features = true
|
||||
"})?;
|
||||
|
||||
// Both enable-all spellings have the same precedence.
|
||||
// Compare against `--preview`.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&enabled,
|
||||
show_settings().arg("--preview-features").arg("pylock"),
|
||||
@""
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test `preview` and `preview-features` parsing in `uv.toml`.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
ignore = "Configuration tests are not yet supported on Windows"
|
||||
)]
|
||||
fn preview_features_uv_toml() -> anyhow::Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
let config = context.temp_dir.child("uv.toml");
|
||||
|
||||
let enabled = capture_uv_snapshot!(
|
||||
context.filters(),
|
||||
add_shared_args(context.version())
|
||||
.arg("--show-settings")
|
||||
.arg("--preview")
|
||||
);
|
||||
|
||||
let disabled = capture_uv_snapshot!(
|
||||
context.filters(),
|
||||
add_shared_args(context.version()).arg("--show-settings")
|
||||
);
|
||||
|
||||
config.write_str(r#"preview-features = ["format-command"]"#)?;
|
||||
|
||||
// A feature list should enable only the named preview features.
|
||||
// Compare against the disabled baseline.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@"
|
||||
...
|
||||
},
|
||||
show_settings: true,
|
||||
preview: Preview {
|
||||
- flags: [],
|
||||
+ flags: [
|
||||
+ Format,
|
||||
+ ],
|
||||
},
|
||||
python_preference: Managed,
|
||||
python_downloads: Automatic,
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
config.write_str("preview-features = true")?;
|
||||
|
||||
// `preview-features = true` should enable all preview features.
|
||||
// Compare against `--preview`.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&enabled,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@""
|
||||
);
|
||||
|
||||
config.write_str("preview-features = false")?;
|
||||
|
||||
// `preview-features = false` should be equivalent to `preview = false`.
|
||||
// Compare against the disabled baseline.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@""
|
||||
);
|
||||
|
||||
config.write_str("preview-features = []")?;
|
||||
|
||||
// An empty feature list should not enable any preview features.
|
||||
// Compare against the disabled baseline.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@""
|
||||
);
|
||||
|
||||
config.write_str(indoc::indoc! {r#"
|
||||
preview = true
|
||||
preview-features = ["format-command"]
|
||||
"#})?;
|
||||
|
||||
// The two settings should be rejected when used together.
|
||||
uv_snapshot!(context.filters(), add_shared_args(context.version()).arg("--show-settings"), @"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to parse: `uv.toml`
|
||||
Caused by: cannot specify both `preview` and `preview-features`
|
||||
");
|
||||
|
||||
config.write_str(r#"preview-features = ["unknown-preview-feature"]"#)?;
|
||||
|
||||
// Unknown names should warn without enabling any recognized preview features.
|
||||
// Compare against the disabled baseline.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&disabled,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@"
|
||||
...
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
+warning: Unknown preview feature: `unknown-preview-feature`
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
config.write_str(r#"preview-features = [" "]"#)?;
|
||||
|
||||
// Empty preview feature names should be rejected.
|
||||
uv_snapshot!(context.filters(), add_shared_args(context.version()).arg("--show-settings"), @"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to parse: `uv.toml`
|
||||
Caused by: TOML parse error at line 1, column 20
|
||||
|
|
||||
1 | preview-features = [\" \"]
|
||||
| ^^^^^^
|
||||
preview feature name cannot be empty
|
||||
");
|
||||
|
||||
config.write_str("preview-features = 123")?;
|
||||
|
||||
// Invalid preview feature types should be rejected.
|
||||
uv_snapshot!(context.filters(), add_shared_args(context.version()).arg("--show-settings"), @"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to parse: `uv.toml`
|
||||
Caused by: TOML parse error at line 1, column 20
|
||||
|
|
||||
1 | preview-features = 123
|
||||
| ^^^
|
||||
invalid type: integer `123`, expected a boolean or a list of preview feature names
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test preview setting discovery and diagnostics in `pyproject.toml`.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
ignore = "Configuration tests are not yet supported on Windows"
|
||||
)]
|
||||
fn preview_features_pyproject_toml() -> anyhow::Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
let pyproject = context.temp_dir.child("pyproject.toml");
|
||||
|
||||
let baseline = capture_uv_snapshot!(
|
||||
context.filters(),
|
||||
add_shared_args(context.version()).arg("--show-settings")
|
||||
);
|
||||
|
||||
pyproject.write_str(indoc::indoc! {r#"
|
||||
[tool.uv]
|
||||
preview-features = ["format-command"]
|
||||
"#})?;
|
||||
|
||||
// A feature list should enable only the named preview features.
|
||||
// Compare against the baseline.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&baseline,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@"
|
||||
...
|
||||
},
|
||||
show_settings: true,
|
||||
preview: Preview {
|
||||
- flags: [],
|
||||
+ flags: [
|
||||
+ Format,
|
||||
+ ],
|
||||
},
|
||||
python_preference: Managed,
|
||||
python_downloads: Automatic,
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
pyproject.write_str(indoc::indoc! {r#"
|
||||
[tool.uv]
|
||||
preview = true
|
||||
preview-features = ["format-command"]
|
||||
"#})?;
|
||||
|
||||
// Conflicting preview settings should be ignored during settings discovery with a warning.
|
||||
// Compare against the baseline.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&baseline,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@"
|
||||
...
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
+warning: Failed to parse `pyproject.toml` during settings discovery:
|
||||
+ TOML parse error at line 1, column 1
|
||||
+ |
|
||||
+ 1 | [tool.uv]
|
||||
+ | ^^^^^^^^^
|
||||
+ cannot specify both `preview` and `preview-features`
|
||||
+
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
pyproject.write_str(indoc::indoc! {r"
|
||||
[tool.uv]
|
||||
preview-features = 123
|
||||
"})?;
|
||||
|
||||
// Invalid preview feature types should be ignored during settings discovery with a warning.
|
||||
// Compare against the baseline.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&baseline,
|
||||
add_shared_args(context.version()).arg("--show-settings"),
|
||||
@"
|
||||
...
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
+warning: Failed to parse `pyproject.toml` during settings discovery:
|
||||
+ TOML parse error at line 2, column 20
|
||||
+ |
|
||||
+ 2 | preview-features = 123
|
||||
+ | ^^^
|
||||
+ invalid type: integer `123`, expected a boolean or a list of preview feature names
|
||||
+
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test PEP 723-specific preview parsing, precedence, and diagnostics.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
ignore = "Configuration tests are not yet supported on Windows"
|
||||
)]
|
||||
fn run_pep723_script_preview_features() -> anyhow::Result<()> {
|
||||
let context = uv_test::test_context!("3.12");
|
||||
|
||||
let show_settings = || {
|
||||
let mut cmd = context.run();
|
||||
cmd.arg("--show-settings").arg("main.py");
|
||||
cmd
|
||||
};
|
||||
|
||||
let test_script = context.temp_dir.child("main.py");
|
||||
test_script.write_str(indoc::indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
print("hello")
|
||||
"#
|
||||
})?;
|
||||
|
||||
let baseline = capture_uv_snapshot!(context.filters(), show_settings());
|
||||
|
||||
let enabled = capture_uv_snapshot!(
|
||||
context.filters(),
|
||||
context
|
||||
.run()
|
||||
.arg("--show-settings")
|
||||
.arg("--preview")
|
||||
.arg("main.py")
|
||||
);
|
||||
|
||||
test_script.write_str(indoc::indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# preview-features = ["format-command"]
|
||||
# ///
|
||||
|
||||
print("hello")
|
||||
"#
|
||||
})?;
|
||||
|
||||
let preview_features = diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&baseline,
|
||||
show_settings(),
|
||||
@"
|
||||
...
|
||||
},
|
||||
show_settings: true,
|
||||
preview: Preview {
|
||||
- flags: [],
|
||||
+ flags: [
|
||||
+ Format,
|
||||
+ ],
|
||||
},
|
||||
python_preference: Managed,
|
||||
python_downloads: Never,
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
test_script.write_str(indoc::indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# preview-features = ["format-command", "unknown-preview-feature"]
|
||||
# ///
|
||||
|
||||
print("hello")
|
||||
"#
|
||||
})?;
|
||||
|
||||
// Unknown names should warn without changing the recognized feature set.
|
||||
// Compare against the recognized feature list to isolate the warning.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&preview_features,
|
||||
show_settings(),
|
||||
@"
|
||||
...
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
+warning: Unknown preview feature: `unknown-preview-feature`
|
||||
...
|
||||
"
|
||||
);
|
||||
|
||||
test_script.write_str(indoc::indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# preview = true
|
||||
# ///
|
||||
|
||||
print("hello")
|
||||
"#
|
||||
})?;
|
||||
|
||||
// The legacy `preview = true` setting should enable all preview features.
|
||||
// Compare against `--preview`.
|
||||
diff_uv_snapshot!(
|
||||
context.filters(),
|
||||
&enabled,
|
||||
show_settings(),
|
||||
@""
|
||||
);
|
||||
|
||||
test_script.write_str(indoc::indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# preview-features = [123]
|
||||
# ///
|
||||
|
||||
print("hello")
|
||||
"#
|
||||
})?;
|
||||
|
||||
// The diagnostic should reject a non-string preview feature name.
|
||||
uv_snapshot!(context.filters(), show_settings(), @"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: TOML parse error at line 4, column 1
|
||||
|
|
||||
4 | [tool.uv]
|
||||
| ^^^^^^^^^
|
||||
invalid type: integer `123`, expected a string
|
||||
");
|
||||
|
||||
context
|
||||
.temp_dir
|
||||
.child("pyproject.toml")
|
||||
.write_str(indoc::indoc! {r"
|
||||
[tool.uv]
|
||||
preview = true
|
||||
"})?;
|
||||
test_script.write_str(indoc::indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# preview-features = false
|
||||
# ///
|
||||
|
||||
print("hello")
|
||||
"#
|
||||
})?;
|
||||
|
||||
// The script setting should atomically override the project setting.
|
||||
assert_eq!(
|
||||
baseline,
|
||||
capture_uv_snapshot!(context.filters(), show_settings())
|
||||
);
|
||||
|
||||
test_script.write_str(indoc::indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.uv]
|
||||
# preview = true
|
||||
# preview-features = ["format-command"]
|
||||
# ///
|
||||
|
||||
print("hello")
|
||||
"#
|
||||
})?;
|
||||
|
||||
// The two settings should be rejected when used together.
|
||||
uv_snapshot!(context.filters(), show_settings(), @"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: TOML parse error at line 4, column 1
|
||||
|
|
||||
4 | [tool.uv]
|
||||
| ^^^^^^^^^
|
||||
cannot specify both `preview` and `preview-features`
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
|
||||
@@ -41,7 +41,20 @@ The `UV_PREVIEW_FEATURES` environment variable can be used similarly, e.g.:
|
||||
$ UV_PREVIEW_FEATURES=foo,bar uv run ...
|
||||
```
|
||||
|
||||
For backwards compatibility, enabling preview features that do not exist will warn, but not error.
|
||||
Preview features can also be enabled in `uv.toml`, or under `[tool.uv]` in `pyproject.toml` and PEP
|
||||
723 metadata:
|
||||
|
||||
```toml
|
||||
preview-features = ["foo", "bar"]
|
||||
```
|
||||
|
||||
Set `preview-features = true` to enable all preview features.
|
||||
|
||||
Some preview features take effect before configuration files are loaded and cannot be enabled from
|
||||
configuration.
|
||||
|
||||
For backwards compatibility, enabling preview features that do not exist will warn, but not error,
|
||||
regardless of the source.
|
||||
|
||||
## Using preview features
|
||||
|
||||
@@ -71,6 +84,12 @@ The following preview features are available:
|
||||
- `workspace-metadata`: Allows using `uv workspace metadata`.
|
||||
- `workspace-dir`: Allows using `uv workspace dir`.
|
||||
- `workspace-list`: Allows using `uv workspace list`.
|
||||
- `target-workspace-discovery`: Uses the directory containing a local `uv run` target, rather than
|
||||
the current working directory, as the starting point for project and workspace discovery. This
|
||||
feature takes effect before configuration is loaded.
|
||||
- `project-directory-must-exist`: Rejects an invalid `--project` path instead of warning and
|
||||
continuing. Except for `uv init`, the path must already exist as a directory or point to a
|
||||
`pyproject.toml` file. This feature takes effect before configuration is loaded.
|
||||
- `malware-check`: Allows `uv sync` and other commands to check for malware using
|
||||
[OSV](https://osv.dev) before installing packages.
|
||||
|
||||
|
||||
Generated
+78
-2
@@ -432,8 +432,20 @@
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"description": "Whether to enable experimental, preview features.",
|
||||
"type": ["boolean", "null"]
|
||||
"description": "Whether to enable all experimental, preview features.\n\nUse `preview-features` instead.",
|
||||
"type": ["boolean", "null"],
|
||||
"deprecated": true
|
||||
},
|
||||
"preview-features": {
|
||||
"description": "Whether to enable specific or all experimental preview features.\n\nUnknown feature names are ignored with a warning.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PreviewFeaturesOption"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"publish-url": {
|
||||
"description": "The URL for publishing packages to the Python package index (by default:\n<https://upload.pypi.org/legacy/>).",
|
||||
@@ -581,6 +593,9 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"not": {
|
||||
"required": ["preview", "preview-features"]
|
||||
},
|
||||
"definitions": {
|
||||
"AddBoundsKind": {
|
||||
"description": "The default version specifier when adding a dependency.",
|
||||
@@ -1619,6 +1634,67 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"PreviewFeature": {
|
||||
"type": "string",
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"python-install-default",
|
||||
"python-upgrade",
|
||||
"json-output",
|
||||
"pylock",
|
||||
"add-bounds",
|
||||
"package-conflicts",
|
||||
"extra-build-dependencies",
|
||||
"detect-module-conflicts",
|
||||
"format-command",
|
||||
"native-auth",
|
||||
"s3-endpoint",
|
||||
"cache-size",
|
||||
"init-project-flag",
|
||||
"workspace-metadata",
|
||||
"workspace-dir",
|
||||
"workspace-list",
|
||||
"sbom-export",
|
||||
"auth-helper",
|
||||
"direct-publish",
|
||||
"target-workspace-discovery",
|
||||
"metadata-json",
|
||||
"gcs-endpoint",
|
||||
"adjust-ulimit",
|
||||
"special-conda-env-names",
|
||||
"relocatable-envs-default",
|
||||
"publish-require-normalized",
|
||||
"audit-command",
|
||||
"project-directory-must-exist",
|
||||
"index-exclude-newer",
|
||||
"azure-endpoint",
|
||||
"toml-backwards-compatibility",
|
||||
"malware-check",
|
||||
"venv-safe-clear",
|
||||
"check-command",
|
||||
"packaged-init"
|
||||
]
|
||||
},
|
||||
{
|
||||
"pattern": "\\S"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PreviewFeaturesOption": {
|
||||
"description": "Represents the `preview-features` configuration option.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PreviewFeature"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ProxyUrl": {
|
||||
"description": "A proxy URL (e.g., `http://proxy.example.com:8080`).",
|
||||
"type": "string",
|
||||
|
||||
Reference in New Issue
Block a user