mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Add OptionsMetadata
macro to uv (#5063)
## Summary The bulk of the change is copied directly from Ruff: -dc8db1afb0/crates/ruff_workspace/src/options_base.rs
-dc8db1afb0/crates/ruff_macros/src/config.rs
This commit is contained in:
parent
c2ef825d7b
commit
8c0ad5b75e
9 changed files with 813 additions and 7 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -2670,9 +2670,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.85"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
|
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
@ -4890,8 +4890,10 @@ dependencies = [
|
||||||
name = "uv-macros"
|
name = "uv-macros"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.71",
|
"syn 2.0.71",
|
||||||
|
"textwrap",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -107,6 +107,7 @@ path-slash = { version = "0.2.1" }
|
||||||
pathdiff = { version = "0.2.1" }
|
pathdiff = { version = "0.2.1" }
|
||||||
petgraph = { version = "0.6.4" }
|
petgraph = { version = "0.6.4" }
|
||||||
platform-info = { version = "2.0.2" }
|
platform-info = { version = "2.0.2" }
|
||||||
|
proc-macro2 = { version = "1.0.86" }
|
||||||
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "3f0ba760951ab0deeac874b98bb18fc90103fcf7" }
|
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "3f0ba760951ab0deeac874b98bb18fc90103fcf7" }
|
||||||
pyo3 = { version = "0.21.0" }
|
pyo3 = { version = "0.21.0" }
|
||||||
pyo3-log = { version = "0.10.0" }
|
pyo3-log = { version = "0.10.0" }
|
||||||
|
|
|
@ -10,5 +10,7 @@ proc-macro = true
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
proc-macro2 = { workspace = true }
|
||||||
quote = { workspace = true }
|
quote = { workspace = true }
|
||||||
syn = { workspace = true }
|
syn = { workspace = true }
|
||||||
|
textwrap = { workspace = true }
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
|
mod options_metadata;
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{parse_macro_input, DeriveInput};
|
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)]
|
#[proc_macro_derive(CombineOptions)]
|
||||||
pub fn derive_combine(input: TokenStream) -> TokenStream {
|
pub fn derive_combine(input: TokenStream) -> TokenStream {
|
||||||
let input = parse_macro_input!(input as DeriveInput);
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
|
358
crates/uv-macros/src/options_metadata.rs
Normal file
358
crates/uv-macros/src/options_metadata.rs
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
//! Taken directly from Ruff.
|
||||||
|
//!
|
||||||
|
//! See: <https://github.com/astral-sh/ruff/blob/dc8db1afb08704ad6a788c497068b01edf8b460d/crates/ruff_macros/src/config.rs>
|
||||||
|
|
||||||
|
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<TokenStream> {
|
||||||
|
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::<syn::Result<Vec<_>>>()?
|
||||||
|
.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<Foobar>` 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<proc_macro2::TokenStream> {
|
||||||
|
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<String> {
|
||||||
|
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<proc_macro2::TokenStream> {
|
||||||
|
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::<syn::Result<Vec<_>>>()?
|
||||||
|
.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<String>) -> 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_field_attributes(attribute: &Attribute) -> syn::Result<FieldAttributes> {
|
||||||
|
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<DeprecatedAttribute> {
|
||||||
|
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<syn::LitStr> {
|
||||||
|
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<String>,
|
||||||
|
note: Option<String>,
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ pub use crate::combine::*;
|
||||||
pub use crate::settings::*;
|
pub use crate::settings::*;
|
||||||
|
|
||||||
mod combine;
|
mod combine;
|
||||||
|
pub mod options_base;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
|
||||||
/// The [`Options`] as loaded from a configuration file on disk.
|
/// The [`Options`] as loaded from a configuration file on disk.
|
||||||
|
|
422
crates/uv-settings/src/options_base.rs
Normal file
422
crates/uv-settings/src/options_base.rs
Normal file
|
@ -0,0 +1,422 @@
|
||||||
|
//! Taken directly from Ruff.
|
||||||
|
//!
|
||||||
|
//! See: <https://github.com/astral-sh/ruff/blob/dc8db1afb08704ad6a788c497068b01edf8b460d/crates/ruff_workspace/src/options_base.rs>
|
||||||
|
|
||||||
|
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::<Self>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> OptionsMetadata for Option<T>
|
||||||
|
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<T>() -> 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<OptionEntry> {
|
||||||
|
struct FindOptionVisitor<'a> {
|
||||||
|
option: Option<OptionEntry>,
|
||||||
|
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<String, OptionField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
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<Deprecated>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ use pypi_types::VerbatimParsedUrl;
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
|
ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
|
||||||
};
|
};
|
||||||
use uv_macros::CombineOptions;
|
use uv_macros::{CombineOptions, OptionsMetadata};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
use uv_python::{PythonFetch, PythonPreference, PythonVersion};
|
use uv_python::{PythonFetch, PythonPreference, PythonVersion};
|
||||||
use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode};
|
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://...`."
|
description = "PEP 508 style requirements, e.g. `flask==3.0.0`, or `black @ https://...`."
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub override_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
pub override_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global settings, relevant to all invocations.
|
/// Global settings, relevant to all invocations.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Default, Deserialize, CombineOptions)]
|
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct GlobalOptions {
|
pub struct GlobalOptions {
|
||||||
|
@ -116,7 +116,7 @@ pub struct ResolverOptions {
|
||||||
/// Shared settings, relevant to all operations that must resolve and install dependencies. The
|
/// Shared settings, relevant to all operations that must resolve and install dependencies. The
|
||||||
/// union of [`InstallerOptions`] and [`ResolverOptions`].
|
/// union of [`InstallerOptions`] and [`ResolverOptions`].
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Default, Deserialize, CombineOptions)]
|
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct ResolverInstallerOptions {
|
pub struct ResolverInstallerOptions {
|
||||||
|
@ -144,7 +144,7 @@ pub struct ResolverInstallerOptions {
|
||||||
|
|
||||||
/// A `[tool.uv.pip]` section.
|
/// A `[tool.uv.pip]` section.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Default, Deserialize, CombineOptions)]
|
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
|
||||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct PipOptions {
|
pub struct PipOptions {
|
||||||
|
@ -174,6 +174,14 @@ pub struct PipOptions {
|
||||||
pub no_strip_extras: Option<bool>,
|
pub no_strip_extras: Option<bool>,
|
||||||
pub no_strip_markers: Option<bool>,
|
pub no_strip_markers: Option<bool>,
|
||||||
pub no_annotate: Option<bool>,
|
pub no_annotate: Option<bool>,
|
||||||
|
/// 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<bool>,
|
pub no_header: Option<bool>,
|
||||||
pub custom_compile_command: Option<String>,
|
pub custom_compile_command: Option<String>,
|
||||||
pub generate_hashes: Option<bool>,
|
pub generate_hashes: Option<bool>,
|
||||||
|
|
1
uv.schema.json
generated
1
uv.schema.json
generated
|
@ -679,6 +679,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"no-header": {
|
"no-header": {
|
||||||
|
"description": "Exclude the comment header at the top of output file generated by `uv pip compile`.",
|
||||||
"type": [
|
"type": [
|
||||||
"boolean",
|
"boolean",
|
||||||
"null"
|
"null"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue