mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 12:24:15 +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
|
|
@ -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.
|
||||
|
|
|
|||
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::{
|
||||
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<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
|
||||
pub override_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
|
||||
}
|
||||
|
||||
/// 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<bool>,
|
||||
pub no_strip_markers: 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 custom_compile_command: Option<String>,
|
||||
pub generate_hashes: Option<bool>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue