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:
Charlie Marsh 2024-07-15 15:24:08 -04:00 committed by GitHub
parent c2ef825d7b
commit 8c0ad5b75e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 813 additions and 7 deletions

View file

@ -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.

View 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)
}
}

View file

@ -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>,