diff --git a/Cargo.lock b/Cargo.lock index 7c98ee638..cbfc4e4c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2670,9 +2670,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -4890,8 +4890,10 @@ dependencies = [ name = "uv-macros" version = "0.0.1" dependencies = [ + "proc-macro2", "quote", "syn 2.0.71", + "textwrap", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 53abe3725..4a9b0e3ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,7 @@ path-slash = { version = "0.2.1" } pathdiff = { version = "0.2.1" } petgraph = { version = "0.6.4" } platform-info = { version = "2.0.2" } +proc-macro2 = { version = "1.0.86" } pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "3f0ba760951ab0deeac874b98bb18fc90103fcf7" } pyo3 = { version = "0.21.0" } pyo3-log = { version = "0.10.0" } diff --git a/crates/uv-macros/Cargo.toml b/crates/uv-macros/Cargo.toml index d746eafa2..3a3b6f971 100644 --- a/crates/uv-macros/Cargo.toml +++ b/crates/uv-macros/Cargo.toml @@ -10,5 +10,7 @@ proc-macro = true workspace = true [dependencies] +proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true } +textwrap = { workspace = true } diff --git a/crates/uv-macros/src/lib.rs b/crates/uv-macros/src/lib.rs index c9ceaefbe..d9db61f3d 100644 --- a/crates/uv-macros/src/lib.rs +++ b/crates/uv-macros/src/lib.rs @@ -1,7 +1,18 @@ +mod options_metadata; + use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; +#[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))] +pub fn derive_options_metadata(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + options_metadata::derive_impl(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + #[proc_macro_derive(CombineOptions)] pub fn derive_combine(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/crates/uv-macros/src/options_metadata.rs b/crates/uv-macros/src/options_metadata.rs new file mode 100644 index 000000000..e4fa7778c --- /dev/null +++ b/crates/uv-macros/src/options_metadata.rs @@ -0,0 +1,358 @@ +//! Taken directly from Ruff. +//! +//! See: + +use proc_macro2::{TokenStream, TokenTree}; +use quote::{quote, quote_spanned}; +use syn::meta::ParseNestedMeta; +use syn::spanned::Spanned; +use syn::{ + AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, ExprLit, Field, + Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Type, TypePath, +}; +use textwrap::dedent; + +pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { + let DeriveInput { + ident, + data, + attrs: struct_attributes, + .. + } = input; + + match data { + Data::Struct(DataStruct { + fields: Fields::Named(fields), + .. + }) => { + let mut output = vec![]; + + for field in &fields.named { + if let Some(attr) = field + .attrs + .iter() + .find(|attr| attr.path().is_ident("option")) + { + output.push(handle_option(field, attr)?); + } else if field + .attrs + .iter() + .any(|attr| attr.path().is_ident("option_group")) + { + output.push(handle_option_group(field)?); + } else if let Some(serde) = field + .attrs + .iter() + .find(|attr| attr.path().is_ident("serde")) + { + // If a field has the `serde(flatten)` attribute, flatten the options into the parent + // by calling `Type::record` instead of `visitor.visit_set` + if let (Type::Path(ty), Meta::List(list)) = (&field.ty, &serde.meta) { + for token in list.tokens.clone() { + if let TokenTree::Ident(ident) = token { + if ident == "flatten" { + output.push(quote_spanned!( + ty.span() => (<#ty>::record(visit)) + )); + + break; + } + } + } + } + } + } + + let docs: Vec<&Attribute> = struct_attributes + .iter() + .filter(|attr| attr.path().is_ident("doc")) + .collect(); + + // Convert the list of `doc` attributes into a single string. + let doc = dedent( + &docs + .into_iter() + .map(parse_doc) + .collect::>>()? + .join("\n"), + ) + .trim_matches('\n') + .to_string(); + + let documentation = if doc.is_empty() { + None + } else { + Some(quote!( + fn documentation() -> Option<&'static str> { + Some(&#doc) + } + )) + }; + + Ok(quote! { + #[automatically_derived] + impl crate::options_base::OptionsMetadata for #ident { + fn record(visit: &mut dyn crate::options_base::Visit) { + #(#output);* + } + + #documentation + } + }) + } + _ => Err(syn::Error::new( + ident.span(), + "Can only derive ConfigurationOptions from structs with named fields.", + )), + } +} + +/// For a field with type `Option` where `Foobar` itself is a struct +/// deriving `ConfigurationOptions`, create code that calls retrieves options +/// from that group: `Foobar::get_available_options()` +fn handle_option_group(field: &Field) -> syn::Result { + let ident = field + .ident + .as_ref() + .expect("Expected to handle named fields"); + + match &field.ty { + Type::Path(TypePath { + path: Path { segments, .. }, + .. + }) => match segments.first() { + Some(PathSegment { + ident: type_ident, + arguments: + PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }), + }) if type_ident == "Option" => { + let path = &args[0]; + let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span()); + + Ok(quote_spanned!( + ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>())) + )) + } + _ => Err(syn::Error::new( + ident.span(), + "Expected `Option<_>` as type.", + )), + }, + _ => Err(syn::Error::new(ident.span(), "Expected type.")), + } +} + +/// Parse a `doc` attribute into it a string literal. +fn parse_doc(doc: &Attribute) -> syn::Result { + match &doc.meta { + syn::Meta::NameValue(syn::MetaNameValue { + value: + syn::Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }), + .. + }) => Ok(lit_str.value()), + _ => Err(syn::Error::new(doc.span(), "Expected doc attribute.")), + } +} + +/// Parse an `#[option(doc="...", default="...", value_type="...", +/// example="...")]` attribute and return data in the form of an `OptionField`. +fn handle_option(field: &Field, attr: &Attribute) -> syn::Result { + let docs: Vec<&Attribute> = field + .attrs + .iter() + .filter(|attr| attr.path().is_ident("doc")) + .collect(); + + if docs.is_empty() { + return Err(syn::Error::new( + field.span(), + "Missing documentation for field", + )); + } + + // Convert the list of `doc` attributes into a single string. + let doc = dedent( + &docs + .into_iter() + .map(parse_doc) + .collect::>>()? + .join("\n"), + ) + .trim_matches('\n') + .to_string(); + + let ident = field + .ident + .as_ref() + .expect("Expected to handle named fields"); + + let FieldAttributes { + default, + value_type, + example, + scope, + } = parse_field_attributes(attr)?; + let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span()); + + let scope = if let Some(scope) = scope { + quote!(Some(#scope)) + } else { + quote!(None) + }; + + let deprecated = if let Some(deprecated) = field + .attrs + .iter() + .find(|attr| attr.path().is_ident("deprecated")) + { + fn quote_option(option: Option) -> TokenStream { + match option { + None => quote!(None), + Some(value) => quote!(Some(#value)), + } + } + + let deprecated = parse_deprecated_attribute(deprecated)?; + let note = quote_option(deprecated.note); + let since = quote_option(deprecated.since); + + quote!(Some(crate::options_base::Deprecated { since: #since, message: #note })) + } else { + quote!(None) + }; + + Ok(quote_spanned!( + ident.span() => { + visit.record_field(#kebab_name, crate::options_base::OptionField{ + doc: &#doc, + default: &#default, + value_type: &#value_type, + example: &#example, + scope: #scope, + deprecated: #deprecated + }) + } + )) +} + +#[derive(Debug)] +struct FieldAttributes { + default: String, + value_type: String, + example: String, + scope: Option, +} + +fn parse_field_attributes(attribute: &Attribute) -> syn::Result { + let mut default = None; + let mut value_type = None; + let mut example = None; + let mut scope = None; + + attribute.parse_nested_meta(|meta| { + if meta.path.is_ident("default") { + default = Some(get_string_literal(&meta, "default", "option")?.value()); + } else if meta.path.is_ident("value_type") { + value_type = Some(get_string_literal(&meta, "value_type", "option")?.value()); + } else if meta.path.is_ident("scope") { + scope = Some(get_string_literal(&meta, "scope", "option")?.value()); + } else if meta.path.is_ident("example") { + let example_text = get_string_literal(&meta, "value_type", "option")?.value(); + example = Some(dedent(&example_text).trim_matches('\n').to_string()); + } else { + return Err(syn::Error::new( + meta.path.span(), + format!( + "Deprecated meta {:?} is not supported by ruff's option macro.", + meta.path.get_ident() + ), + )); + } + + Ok(()) + })?; + + let Some(default) = default else { + return Err(syn::Error::new(attribute.span(), "Mandatory `default` field is missing in `#[option]` attribute. Specify the default using `#[option(default=\"..\")]`.")); + }; + + let Some(value_type) = value_type else { + return Err(syn::Error::new(attribute.span(), "Mandatory `value_type` field is missing in `#[option]` attribute. Specify the value type using `#[option(value_type=\"..\")]`.")); + }; + + let Some(example) = example else { + return Err(syn::Error::new(attribute.span(), "Mandatory `example` field is missing in `#[option]` attribute. Add an example using `#[option(example=\"..\")]`.")); + }; + + Ok(FieldAttributes { + default, + value_type, + example, + scope, + }) +} + +fn parse_deprecated_attribute(attribute: &Attribute) -> syn::Result { + let mut deprecated = DeprecatedAttribute::default(); + attribute.parse_nested_meta(|meta| { + if meta.path.is_ident("note") { + deprecated.note = Some(get_string_literal(&meta, "note", "deprecated")?.value()); + } else if meta.path.is_ident("since") { + deprecated.since = Some(get_string_literal(&meta, "since", "deprecated")?.value()); + } else { + return Err(syn::Error::new( + meta.path.span(), + format!( + "Deprecated meta {:?} is not supported by ruff's option macro.", + meta.path.get_ident() + ), + )); + } + + Ok(()) + })?; + + Ok(deprecated) +} + +fn get_string_literal( + meta: &ParseNestedMeta, + meta_name: &str, + attribute_name: &str, +) -> syn::Result { + let expr: syn::Expr = meta.value()?.parse()?; + + let mut value = &expr; + while let syn::Expr::Group(e) = value { + value = &e.expr; + } + + if let syn::Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + }) = value + { + let suffix = lit.suffix(); + if !suffix.is_empty() { + return Err(syn::Error::new( + lit.span(), + format!("unexpected suffix `{suffix}` on string literal"), + )); + } + + Ok(lit.clone()) + } else { + Err(syn::Error::new( + expr.span(), + format!("expected {attribute_name} attribute to be a string: `{meta_name} = \"...\"`"), + )) + } +} + +#[derive(Default, Debug)] +struct DeprecatedAttribute { + since: Option, + note: Option, +} diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 38c5d463e..eb5ca350c 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -10,6 +10,7 @@ pub use crate::combine::*; pub use crate::settings::*; mod combine; +pub mod options_base; mod settings; /// The [`Options`] as loaded from a configuration file on disk. diff --git a/crates/uv-settings/src/options_base.rs b/crates/uv-settings/src/options_base.rs new file mode 100644 index 000000000..ba93ddde9 --- /dev/null +++ b/crates/uv-settings/src/options_base.rs @@ -0,0 +1,422 @@ +//! Taken directly from Ruff. +//! +//! See: + +use serde::{Serialize, Serializer}; +use std::collections::BTreeMap; + +use std::fmt::{Debug, Display, Formatter}; + +/// Visits [`OptionsMetadata`]. +/// +/// An instance of [`Visit`] represents the logic for inspecting an object's options metadata. +pub trait Visit { + /// Visits an [`OptionField`] value named `name`. + fn record_field(&mut self, name: &str, field: OptionField); + + /// Visits an [`OptionSet`] value named `name`. + fn record_set(&mut self, name: &str, group: OptionSet); +} + +/// Returns metadata for its options. +pub trait OptionsMetadata { + /// Visits the options metadata of this object by calling `visit` for each option. + fn record(visit: &mut dyn Visit); + + fn documentation() -> Option<&'static str> { + None + } + + /// Returns the extracted metadata. + fn metadata() -> OptionSet + where + Self: Sized + 'static, + { + OptionSet::of::() + } +} + +impl OptionsMetadata for Option +where + T: OptionsMetadata, +{ + fn record(visit: &mut dyn Visit) { + T::record(visit); + } +} + +/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`]. +#[derive(Clone, PartialEq, Eq, Debug, Serialize)] +#[serde(untagged)] +pub enum OptionEntry { + /// A single option. + Field(OptionField), + + /// A set of options. + Set(OptionSet), +} + +impl Display for OptionEntry { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + OptionEntry::Set(set) => std::fmt::Display::fmt(set, f), + OptionEntry::Field(field) => std::fmt::Display::fmt(&field, f), + } + } +} + +/// A set of options. +/// +/// It extracts the options by calling the [`OptionsMetadata::record`] of a type implementing +/// [`OptionsMetadata`]. +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct OptionSet { + record: fn(&mut dyn Visit), + doc: fn() -> Option<&'static str>, +} + +impl OptionSet { + pub fn of() -> Self + where + T: OptionsMetadata + 'static, + { + Self { + record: T::record, + doc: T::documentation, + } + } + + /// Visits the options in this set by calling `visit` for each option. + pub fn record(&self, visit: &mut dyn Visit) { + let record = self.record; + record(visit); + } + + pub fn documentation(&self) -> Option<&'static str> { + let documentation = self.doc; + documentation() + } + + /// Returns `true` if this set has an option that resolves to `name`. + /// + /// The name can be separated by `.` to find a nested option. + /// + /// ## Examples + /// + /// ### Test for the existence of a child option + /// + /// ```rust + /// # use uv_settings::options_base::{OptionField, OptionsMetadata, Visit}; + /// + /// struct WithOptions; + /// + /// impl OptionsMetadata for WithOptions { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None, + /// }); + /// } + /// } + /// + /// assert!(WithOptions::metadata().has("ignore-git-ignore")); + /// assert!(!WithOptions::metadata().has("does-not-exist")); + /// ``` + /// ### Test for the existence of a nested option + /// + /// ```rust + /// # use uv_settings::options_base::{OptionField, OptionsMetadata, Visit}; + /// + /// struct Root; + /// + /// impl OptionsMetadata for Root { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }); + /// + /// visit.record_set("format", Nested::metadata()); + /// } + /// } + /// + /// struct Nested; + /// + /// impl OptionsMetadata for Nested { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("hard-tabs", OptionField { + /// doc: "Use hard tabs for indentation and spaces for alignment.", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }); + /// } + /// } + /// + /// assert!(Root::metadata().has("format.hard-tabs")); + /// assert!(!Root::metadata().has("format.spaces")); + /// assert!(!Root::metadata().has("lint.hard-tabs")); + /// ``` + pub fn has(&self, name: &str) -> bool { + self.find(name).is_some() + } + + /// Returns `Some` if this set has an option that resolves to `name` and `None` otherwise. + /// + /// The name can be separated by `.` to find a nested option. + /// + /// ## Examples + /// + /// ### Find a child option + /// + /// ```rust + /// # use uv_settings::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit}; + /// + /// struct WithOptions; + /// + /// static IGNORE_GIT_IGNORE: OptionField = OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }; + /// + /// impl OptionsMetadata for WithOptions { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone()); + /// } + /// } + /// + /// assert_eq!(WithOptions::metadata().find("ignore-git-ignore"), Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone()))); + /// assert_eq!(WithOptions::metadata().find("does-not-exist"), None); + /// ``` + /// ### Find a nested option + /// + /// ```rust + /// # use uv_settings::options_base::{OptionEntry, OptionField, OptionsMetadata, Visit}; + /// + /// static HARD_TABS: OptionField = OptionField { + /// doc: "Use hard tabs for indentation and spaces for alignment.", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }; + /// + /// struct Root; + /// + /// impl OptionsMetadata for Root { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("ignore-git-ignore", OptionField { + /// doc: "Whether Ruff should respect the gitignore file", + /// default: "false", + /// value_type: "bool", + /// example: "", + /// scope: None, + /// deprecated: None + /// }); + /// + /// visit.record_set("format", Nested::metadata()); + /// } + /// } + /// + /// struct Nested; + /// + /// impl OptionsMetadata for Nested { + /// fn record(visit: &mut dyn Visit) { + /// visit.record_field("hard-tabs", HARD_TABS.clone()); + /// } + /// } + /// + /// assert_eq!(Root::metadata().find("format.hard-tabs"), Some(OptionEntry::Field(HARD_TABS.clone()))); + /// assert_eq!(Root::metadata().find("format"), Some(OptionEntry::Set(Nested::metadata()))); + /// assert_eq!(Root::metadata().find("format.spaces"), None); + /// assert_eq!(Root::metadata().find("lint.hard-tabs"), None); + /// ``` + pub fn find(&self, name: &str) -> Option { + struct FindOptionVisitor<'a> { + option: Option, + parts: std::str::Split<'a, char>, + needle: &'a str, + } + + impl Visit for FindOptionVisitor<'_> { + fn record_set(&mut self, name: &str, set: OptionSet) { + if self.option.is_none() && name == self.needle { + if let Some(next) = self.parts.next() { + self.needle = next; + set.record(self); + } else { + self.option = Some(OptionEntry::Set(set)); + } + } + } + + fn record_field(&mut self, name: &str, field: OptionField) { + if self.option.is_none() && name == self.needle { + if self.parts.next().is_none() { + self.option = Some(OptionEntry::Field(field)); + } + } + } + } + + let mut parts = name.split('.'); + + if let Some(first) = parts.next() { + let mut visitor = FindOptionVisitor { + parts, + needle: first, + option: None, + }; + + self.record(&mut visitor); + visitor.option + } else { + None + } + } +} + +/// Visitor that writes out the names of all fields and sets. +struct DisplayVisitor<'fmt, 'buf> { + f: &'fmt mut Formatter<'buf>, + result: std::fmt::Result, +} + +impl<'fmt, 'buf> DisplayVisitor<'fmt, 'buf> { + fn new(f: &'fmt mut Formatter<'buf>) -> Self { + Self { f, result: Ok(()) } + } + + fn finish(self) -> std::fmt::Result { + self.result + } +} + +impl Visit for DisplayVisitor<'_, '_> { + fn record_set(&mut self, name: &str, _: OptionSet) { + self.result = self.result.and_then(|()| writeln!(self.f, "{name}")); + } + + fn record_field(&mut self, name: &str, field: OptionField) { + self.result = self.result.and_then(|()| { + write!(self.f, "{name}")?; + + if field.deprecated.is_some() { + write!(self.f, " (deprecated)")?; + } + + writeln!(self.f) + }); + } +} + +impl Display for OptionSet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut visitor = DisplayVisitor::new(f); + self.record(&mut visitor); + visitor.finish() + } +} + +struct SerializeVisitor<'a> { + entries: &'a mut BTreeMap, +} + +impl<'a> Visit for SerializeVisitor<'a> { + fn record_set(&mut self, name: &str, set: OptionSet) { + // Collect the entries of the set. + let mut entries = BTreeMap::new(); + let mut visitor = SerializeVisitor { + entries: &mut entries, + }; + set.record(&mut visitor); + + // Insert the set into the entries. + for (key, value) in entries { + self.entries.insert(format!("{name}.{key}"), value); + } + } + + fn record_field(&mut self, name: &str, field: OptionField) { + self.entries.insert(name.to_string(), field); + } +} + +impl Serialize for OptionSet { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut entries = BTreeMap::new(); + let mut visitor = SerializeVisitor { + entries: &mut entries, + }; + self.record(&mut visitor); + entries.serialize(serializer) + } +} + +impl Debug for OptionSet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Serialize)] +pub struct OptionField { + pub doc: &'static str, + /// Ex) `"false"` + pub default: &'static str, + /// Ex) `"bool"` + pub value_type: &'static str, + /// Ex) `"per-file-ignores"` + pub scope: Option<&'static str>, + pub example: &'static str, + pub deprecated: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub struct Deprecated { + pub since: Option<&'static str>, + pub message: Option<&'static str>, +} + +impl Display for OptionField { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.doc)?; + writeln!(f)?; + writeln!(f, "Default value: {}", self.default)?; + writeln!(f, "Type: {}", self.value_type)?; + + if let Some(deprecated) = &self.deprecated { + write!(f, "Deprecated")?; + + if let Some(since) = deprecated.since { + write!(f, " (since {since})")?; + } + + if let Some(message) = deprecated.message { + write!(f, ": {message}")?; + } + + writeln!(f)?; + } + + writeln!(f, "Example usage:\n```toml\n{}\n```", self.example) + } +} diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index e7a9e8e7b..a6ab3430b 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -9,7 +9,7 @@ use pypi_types::VerbatimParsedUrl; use uv_configuration::{ ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple, }; -use uv_macros::CombineOptions; +use uv_macros::{CombineOptions, OptionsMetadata}; use uv_normalize::{ExtraName, PackageName}; use uv_python::{PythonFetch, PythonPreference, PythonVersion}; use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode}; @@ -46,12 +46,12 @@ pub struct Options { description = "PEP 508 style requirements, e.g. `flask==3.0.0`, or `black @ https://...`." ) )] - pub override_dependencies: Option>>, + pub override_dependencies: Option>>, } /// Global settings, relevant to all invocations. #[allow(dead_code)] -#[derive(Debug, Clone, Default, Deserialize, CombineOptions)] +#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct GlobalOptions { @@ -116,7 +116,7 @@ pub struct ResolverOptions { /// Shared settings, relevant to all operations that must resolve and install dependencies. The /// union of [`InstallerOptions`] and [`ResolverOptions`]. #[allow(dead_code)] -#[derive(Debug, Clone, Default, Deserialize, CombineOptions)] +#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ResolverInstallerOptions { @@ -144,7 +144,7 @@ pub struct ResolverInstallerOptions { /// A `[tool.uv.pip]` section. #[allow(dead_code)] -#[derive(Debug, Clone, Default, Deserialize, CombineOptions)] +#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct PipOptions { @@ -174,6 +174,14 @@ pub struct PipOptions { pub no_strip_extras: Option, pub no_strip_markers: Option, pub no_annotate: Option, + /// Exclude the comment header at the top of output file generated by `uv pip compile`. + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + no-header = true + "# + )] pub no_header: Option, pub custom_compile_command: Option, pub generate_hashes: Option, diff --git a/uv.schema.json b/uv.schema.json index 568c92fc7..4fa71c265 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -679,6 +679,7 @@ } }, "no-header": { + "description": "Exclude the comment header at the top of output file generated by `uv pip compile`.", "type": [ "boolean", "null"