use std::fmt; use std::path::Path; use std::str::FromStr; use ruff_formatter::printer::{LineEnding, PrinterOptions, SourceMapGeneration}; use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth}; use ruff_macros::CacheKey; use ruff_python_ast::{self as ast, PySourceType}; /// Resolved options for formatting one individual file. The difference to `FormatterSettings` /// is that `FormatterSettings` stores the settings for multiple files (the entire project, a subdirectory, ..) #[derive(Clone, Debug)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default, deny_unknown_fields) )] pub struct PyFormatOptions { /// Whether we're in a `.py` file or `.pyi` file, which have different rules. source_type: PySourceType, /// The (minimum) Python version used to run the formatted code. This is used /// to determine the supported Python syntax. target_version: ast::PythonVersion, /// Specifies the indent style: /// * Either a tab /// * or a specific amount of spaces #[cfg_attr(feature = "serde", serde(default = "default_indent_style"))] indent_style: IndentStyle, /// The preferred line width at which the formatter should wrap lines. #[cfg_attr(feature = "serde", serde(default = "default_line_width"))] line_width: LineWidth, /// The visual width of a tab character. #[cfg_attr(feature = "serde", serde(default = "default_indent_width"))] indent_width: IndentWidth, line_ending: LineEnding, /// The preferred quote style to use (single vs double quotes). quote_style: QuoteStyle, /// Whether to expand lists or elements if they have a trailing comma such as `(a, b,)`. magic_trailing_comma: MagicTrailingComma, /// Should the formatter generate a source map that allows mapping source positions to positions /// in the formatted document. source_map_generation: SourceMapGeneration, /// Whether to format code snippets in docstrings or not. /// /// By default this is disabled (opt-in), but the plan is to make this /// enabled by default (opt-out) in the future. docstring_code: DocstringCode, /// The preferred line width at which the formatter should wrap lines in /// docstring code examples. This only has an impact when `docstring_code` /// is enabled. docstring_code_line_width: DocstringCodeLineWidth, /// Whether preview style formatting is enabled or not preview: PreviewMode, } fn default_line_width() -> LineWidth { LineWidth::try_from(88).unwrap() } fn default_indent_style() -> IndentStyle { IndentStyle::Space } fn default_indent_width() -> IndentWidth { IndentWidth::try_from(4).unwrap() } impl Default for PyFormatOptions { fn default() -> Self { Self { source_type: PySourceType::default(), target_version: ast::PythonVersion::default(), indent_style: default_indent_style(), line_width: default_line_width(), indent_width: default_indent_width(), quote_style: QuoteStyle::default(), line_ending: LineEnding::default(), magic_trailing_comma: MagicTrailingComma::default(), source_map_generation: SourceMapGeneration::default(), docstring_code: DocstringCode::default(), docstring_code_line_width: DocstringCodeLineWidth::default(), preview: PreviewMode::default(), } } } impl PyFormatOptions { /// Otherwise sets the defaults. Returns none if the extension is unknown pub fn from_extension(path: &Path) -> Self { Self::from_source_type(PySourceType::from(path)) } pub fn from_source_type(source_type: PySourceType) -> Self { Self { source_type, ..Self::default() } } pub const fn target_version(&self) -> ast::PythonVersion { self.target_version } pub const fn magic_trailing_comma(&self) -> MagicTrailingComma { self.magic_trailing_comma } pub const fn quote_style(&self) -> QuoteStyle { self.quote_style } pub const fn source_type(&self) -> PySourceType { self.source_type } pub const fn source_map_generation(&self) -> SourceMapGeneration { self.source_map_generation } pub const fn line_ending(&self) -> LineEnding { self.line_ending } pub const fn docstring_code(&self) -> DocstringCode { self.docstring_code } pub const fn docstring_code_line_width(&self) -> DocstringCodeLineWidth { self.docstring_code_line_width } pub const fn preview(&self) -> PreviewMode { self.preview } #[must_use] pub fn with_target_version(mut self, target_version: ast::PythonVersion) -> Self { self.target_version = target_version; self } #[must_use] pub fn with_indent_width(mut self, indent_width: IndentWidth) -> Self { self.indent_width = indent_width; self } #[must_use] pub fn with_quote_style(mut self, style: QuoteStyle) -> Self { self.quote_style = style; self } #[must_use] pub fn with_magic_trailing_comma(mut self, trailing_comma: MagicTrailingComma) -> Self { self.magic_trailing_comma = trailing_comma; self } #[must_use] pub fn with_indent_style(mut self, indent_style: IndentStyle) -> Self { self.indent_style = indent_style; self } #[must_use] pub fn with_line_width(mut self, line_width: LineWidth) -> Self { self.line_width = line_width; self } #[must_use] pub fn with_line_ending(mut self, line_ending: LineEnding) -> Self { self.line_ending = line_ending; self } #[must_use] pub fn with_docstring_code(mut self, docstring_code: DocstringCode) -> Self { self.docstring_code = docstring_code; self } #[must_use] pub fn with_docstring_code_line_width(mut self, line_width: DocstringCodeLineWidth) -> Self { self.docstring_code_line_width = line_width; self } #[must_use] pub fn with_preview(mut self, preview: PreviewMode) -> Self { self.preview = preview; self } #[must_use] pub fn with_source_map_generation(mut self, source_map: SourceMapGeneration) -> Self { self.source_map_generation = source_map; self } } impl FormatOptions for PyFormatOptions { fn indent_style(&self) -> IndentStyle { self.indent_style } fn indent_width(&self) -> IndentWidth { self.indent_width } fn line_width(&self) -> LineWidth { self.line_width } fn as_print_options(&self) -> PrinterOptions { PrinterOptions { indent_width: self.indent_width, line_width: self.line_width, line_ending: self.line_ending, indent_style: self.indent_style, } } } #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, CacheKey)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "kebab-case") )] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum QuoteStyle { Single, #[default] Double, Preserve, } impl QuoteStyle { pub const fn is_preserve(self) -> bool { matches!(self, QuoteStyle::Preserve) } } impl fmt::Display for QuoteStyle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Single => write!(f, "single"), Self::Double => write!(f, "double"), Self::Preserve => write!(f, "preserve"), } } } impl FromStr for QuoteStyle { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "\"" | "double" | "Double" => Ok(Self::Double), "'" | "single" | "Single" => Ok(Self::Single), "preserve" | "Preserve" => Ok(Self::Preserve), // TODO: replace this error with a diagnostic _ => Err("Value not supported for QuoteStyle"), } } } #[derive(Copy, Clone, Debug, Default, CacheKey)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "kebab-case") )] pub enum MagicTrailingComma { #[default] Respect, Ignore, } impl MagicTrailingComma { pub const fn is_respect(self) -> bool { matches!(self, Self::Respect) } pub const fn is_ignore(self) -> bool { matches!(self, Self::Ignore) } } impl fmt::Display for MagicTrailingComma { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Respect => write!(f, "respect"), Self::Ignore => write!(f, "ignore"), } } } impl FromStr for MagicTrailingComma { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "respect" | "Respect" => Ok(Self::Respect), "ignore" | "Ignore" => Ok(Self::Ignore), // TODO: replace this error with a diagnostic _ => Err("Value not supported for MagicTrailingComma"), } } } #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] pub enum PreviewMode { #[default] Disabled, Enabled, } impl PreviewMode { pub const fn is_enabled(self) -> bool { matches!(self, PreviewMode::Enabled) } } impl fmt::Display for PreviewMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Disabled => write!(f, "disabled"), Self::Enabled => write!(f, "enabled"), } } } #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum DocstringCode { #[default] Disabled, Enabled, } impl DocstringCode { pub const fn is_enabled(self) -> bool { matches!(self, DocstringCode::Enabled) } } impl fmt::Display for DocstringCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Disabled => write!(f, "disabled"), Self::Enabled => write!(f, "enabled"), } } } #[derive(Copy, Clone, Default, Eq, PartialEq, CacheKey)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged, rename_all = "lowercase") )] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum DocstringCodeLineWidth { /// Wrap docstring code examples at a fixed line width. #[cfg_attr(feature = "schemars", schemars(schema_with = "schema::fixed"))] Fixed(LineWidth), /// Respect the line length limit setting for the surrounding Python code. #[default] #[cfg_attr( feature = "serde", serde(deserialize_with = "deserialize_docstring_code_line_width_dynamic") )] #[cfg_attr(feature = "schemars", schemars(schema_with = "schema::dynamic"))] Dynamic, } #[cfg(feature = "schemars")] mod schema { use ruff_formatter::LineWidth; use schemars::r#gen::SchemaGenerator; use schemars::schema::{Metadata, Schema, SubschemaValidation}; /// A dummy type that is used to generate a schema for `DocstringCodeLineWidth::Dynamic`. pub(super) fn dynamic(_: &mut SchemaGenerator) -> Schema { Schema::Object(schemars::schema::SchemaObject { const_value: Some("dynamic".to_string().into()), ..Default::default() }) } // We use a manual schema for `fixed` even thought it isn't strictly necessary according to the // JSON schema specification to work around a bug in Even Better TOML with `allOf`. // https://github.com/astral-sh/ruff/issues/15978#issuecomment-2639547101 // // The only difference to the automatically derived schema is that we use `oneOf` instead of // `allOf`. There's no semantic difference between `allOf` and `oneOf` for single element lists. pub(super) fn fixed(generator: &mut SchemaGenerator) -> Schema { let schema = generator.subschema_for::(); Schema::Object(schemars::schema::SchemaObject { metadata: Some(Box::new(Metadata { description: Some( "Wrap docstring code examples at a fixed line width.".to_string(), ), ..Metadata::default() })), subschemas: Some(Box::new(SubschemaValidation { one_of: Some(vec![schema]), ..SubschemaValidation::default() })), ..Default::default() }) } } impl fmt::Debug for DocstringCodeLineWidth { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { DocstringCodeLineWidth::Fixed(v) => v.value().fmt(f), DocstringCodeLineWidth::Dynamic => "dynamic".fmt(f), } } } impl fmt::Display for DocstringCodeLineWidth { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Fixed(width) => width.fmt(f), Self::Dynamic => write!(f, "dynamic"), } } } /// Responsible for deserializing the `DocstringCodeLineWidth::Dynamic` /// variant. fn deserialize_docstring_code_line_width_dynamic<'de, D>(d: D) -> Result<(), D::Error> where D: serde::Deserializer<'de>, { use serde::{Deserialize, de::Error}; let value = String::deserialize(d)?; match &*value { "dynamic" => Ok(()), s => Err(D::Error::invalid_value( serde::de::Unexpected::Str(s), &"dynamic", )), } }