diff --git a/crates/ruff_benchmark/benches/formatter.rs b/crates/ruff_benchmark/benches/formatter.rs index 15698dbba0..74688bed09 100644 --- a/crates/ruff_benchmark/benches/formatter.rs +++ b/crates/ruff_benchmark/benches/formatter.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use ruff_benchmark::{TestCase, TestCaseSpeed, TestFile, TestFileDownloadError}; -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use std::time::Duration; #[cfg(target_os = "windows")] @@ -50,7 +50,10 @@ fn benchmark_formatter(criterion: &mut Criterion) { BenchmarkId::from_parameter(case.name()), &case, |b, case| { - b.iter(|| format_module(case.code()).expect("Formatting to succeed")); + b.iter(|| { + format_module(case.code(), PyFormatOptions::default()) + .expect("Formatting to succeed") + }); }, ); } diff --git a/crates/ruff_cli/src/lib.rs b/crates/ruff_cli/src/lib.rs index 4b63ad4a07..0f737628d1 100644 --- a/crates/ruff_cli/src/lib.rs +++ b/crates/ruff_cli/src/lib.rs @@ -13,7 +13,7 @@ use ruff::logging::{set_up_logging, LogLevel}; use ruff::settings::types::SerializationFormat; use ruff::settings::{flags, CliSettings}; use ruff::{fs, warn_user_once}; -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use crate::args::{Args, CheckArgs, Command}; use crate::commands::run_stdin::read_from_stdin; @@ -137,7 +137,7 @@ fn format(files: &[PathBuf]) -> Result { // dummy, to check that the function was actually called let contents = code.replace("# DEL", ""); // real formatting that is currently a passthrough - format_module(&contents) + format_module(&contents, PyFormatOptions::default()) }; match &files { diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs index 39f90224b2..4f07db548f 100644 --- a/crates/ruff_dev/src/check_formatter_stability.rs +++ b/crates/ruff_dev/src/check_formatter_stability.rs @@ -11,7 +11,7 @@ use ruff::resolver::python_files_in_path; use ruff::settings::types::{FilePattern, FilePatternSet}; use ruff_cli::args::CheckArgs; use ruff_cli::resolve::resolve; -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use similar::{ChangeTag, TextDiff}; use std::io::Write; use std::panic::catch_unwind; @@ -276,7 +276,7 @@ impl From for FormatterStabilityError { /// Run the formatter twice on the given file. Does not write back to the file fn check_file(input_path: &Path) -> Result<(), FormatterStabilityError> { let content = fs::read_to_string(input_path).context("Failed to read file")?; - let printed = match format_module(&content) { + let printed = match format_module(&content, PyFormatOptions::default()) { Ok(printed) => printed, Err(err) => { return if err @@ -296,7 +296,7 @@ fn check_file(input_path: &Path) -> Result<(), FormatterStabilityError> { }; let formatted = printed.as_code(); - let reformatted = match format_module(formatted) { + let reformatted = match format_module(formatted, PyFormatOptions::default()) { Ok(reformatted) => reformatted, Err(err) => { return Err(FormatterStabilityError::InvalidSyntax { diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 50a060e423..db52ed9b91 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -1,7 +1,6 @@ use crate::context::NodeLevel; use crate::prelude::*; use crate::trivia::{first_non_trivia_token, lines_after, skip_trailing_trivia, Token, TokenKind}; -use crate::USE_MAGIC_TRAILING_COMMA; use ruff_formatter::write; use ruff_text_size::TextSize; use rustpython_parser::ast::Ranged; @@ -221,7 +220,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { if let Some(last_end) = self.last_end.take() { if_group_breaks(&text(",")).fmt(self.fmt)?; - if USE_MAGIC_TRAILING_COMMA + if self.fmt.options().magic_trailing_comma().is_preserve() && matches!( first_non_trivia_token(last_end, self.fmt.context().contents()), Some(Token { @@ -243,8 +242,8 @@ mod tests { use crate::comments::Comments; use crate::context::{NodeLevel, PyFormatContext}; use crate::prelude::*; + use crate::PyFormatOptions; use ruff_formatter::format; - use ruff_formatter::SimpleFormatOptions; use rustpython_parser::ast::ModModule; use rustpython_parser::Parse; @@ -265,8 +264,7 @@ no_leading_newline = 30 let module = ModModule::parse(source, "test.py").unwrap(); - let context = - PyFormatContext::new(SimpleFormatOptions::default(), source, Comments::default()); + let context = PyFormatContext::new(PyFormatOptions::default(), source, Comments::default()); let test_formatter = format_with(|f: &mut PyFormatter| f.join_nodes(level).nodes(&module.body).finish()); diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs index 8a277ece24..15d480377f 100644 --- a/crates/ruff_python_formatter/src/cli.rs +++ b/crates/ruff_python_formatter/src/cli.rs @@ -10,7 +10,7 @@ use rustpython_parser::{parse_tokens, Mode}; use ruff_formatter::SourceCode; use ruff_python_ast::source_code::CommentRangesBuilder; -use crate::format_node; +use crate::{format_node, PyFormatOptions}; #[derive(ValueEnum, Clone, Debug)] pub enum Emit { @@ -57,7 +57,12 @@ pub fn format_and_debug_print(input: &str, cli: &Cli) -> Result { let python_ast = parse_tokens(tokens, Mode::Module, "") .with_context(|| "Syntax error in input")?; - let formatted = format_node(&python_ast, &comment_ranges, input)?; + let formatted = format_node( + &python_ast, + &comment_ranges, + input, + PyFormatOptions::default(), + )?; if cli.print_ir { println!("{}", formatted.document().display(SourceCode::new(input))); } diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 074d5cc08d..cdf587c35f 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -1,22 +1,19 @@ use crate::comments::Comments; -use ruff_formatter::{FormatContext, SimpleFormatOptions, SourceCode}; +use crate::PyFormatOptions; +use ruff_formatter::{FormatContext, SourceCode}; use ruff_python_ast::source_code::Locator; use std::fmt::{Debug, Formatter}; #[derive(Clone)] pub struct PyFormatContext<'a> { - options: SimpleFormatOptions, + options: PyFormatOptions, contents: &'a str, comments: Comments<'a>, node_level: NodeLevel, } impl<'a> PyFormatContext<'a> { - pub(crate) fn new( - options: SimpleFormatOptions, - contents: &'a str, - comments: Comments<'a>, - ) -> Self { + pub(crate) fn new(options: PyFormatOptions, contents: &'a str, comments: Comments<'a>) -> Self { Self { options, contents, @@ -48,7 +45,7 @@ impl<'a> PyFormatContext<'a> { } impl FormatContext for PyFormatContext<'_> { - type Options = SimpleFormatOptions; + type Options = PyFormatOptions; fn options(&self) -> &Self::Options { &self.options diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 93197b2ca2..4932ca5da0 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -2,10 +2,11 @@ use crate::comments::{ dangling_node_comments, leading_node_comments, trailing_node_comments, Comments, }; use crate::context::PyFormatContext; +pub use crate::options::{MagicTrailingComma, PyFormatOptions, QuoteStyle}; use anyhow::{anyhow, Context, Result}; use ruff_formatter::prelude::*; use ruff_formatter::{format, write}; -use ruff_formatter::{Formatted, IndentStyle, Printed, SimpleFormatOptions, SourceCode}; +use ruff_formatter::{Formatted, Printed, SourceCode}; use ruff_python_ast::node::{AnyNodeRef, AstNode, NodeKind}; use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder, Locator}; use ruff_text_size::{TextLen, TextRange}; @@ -21,6 +22,7 @@ pub(crate) mod context; pub(crate) mod expression; mod generated; pub(crate) mod module; +mod options; pub(crate) mod other; pub(crate) mod pattern; mod prelude; @@ -29,10 +31,6 @@ mod trivia; include!("../../ruff_formatter/shared_traits.rs"); -/// TODO(konstin): hook this up to the settings by replacing `SimpleFormatOptions` with a python -/// specific struct. -pub(crate) const USE_MAGIC_TRAILING_COMMA: bool = true; - /// 'ast is the lifetime of the source code (input), 'buf is the lifetime of the buffer (output) pub(crate) type PyFormatter<'ast, 'buf> = Formatter<'buf, PyFormatContext<'ast>>; @@ -86,7 +84,7 @@ where } } -pub fn format_module(contents: &str) -> Result { +pub fn format_module(contents: &str, options: PyFormatOptions) -> Result { // Tokenize once let mut tokens = Vec::new(); let mut comment_ranges = CommentRangesBuilder::default(); @@ -107,7 +105,7 @@ pub fn format_module(contents: &str) -> Result { let python_ast = parse_tokens(tokens, Mode::Module, "") .with_context(|| "Syntax error in input")?; - let formatted = format_node(&python_ast, &comment_ranges, contents)?; + let formatted = format_node(&python_ast, &comment_ranges, contents, options)?; formatted .print() @@ -118,20 +116,14 @@ pub fn format_node<'a>( root: &'a Mod, comment_ranges: &'a CommentRanges, source: &'a str, + options: PyFormatOptions, ) -> FormatResult>> { let comments = Comments::from_ast(root, SourceCode::new(source), comment_ranges); let locator = Locator::new(source); format!( - PyFormatContext::new( - SimpleFormatOptions { - indent_style: IndentStyle::Space(4), - line_width: 88.try_into().unwrap(), - }, - locator.contents(), - comments, - ), + PyFormatContext::new(options, locator.contents(), comments), [root.format()] ) } @@ -226,44 +218,9 @@ impl Format> for VerbatimText { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum QuoteStyle { - Single, - Double, -} - -impl QuoteStyle { - pub const fn as_char(self) -> char { - match self { - QuoteStyle::Single => '\'', - QuoteStyle::Double => '"', - } - } - - #[must_use] - pub const fn opposite(self) -> QuoteStyle { - match self { - QuoteStyle::Single => QuoteStyle::Double, - QuoteStyle::Double => QuoteStyle::Single, - } - } -} - -impl TryFrom for QuoteStyle { - type Error = (); - - fn try_from(value: char) -> std::result::Result { - match value { - '\'' => Ok(QuoteStyle::Single), - '"' => Ok(QuoteStyle::Double), - _ => Err(()), - } - } -} - #[cfg(test)] mod tests { - use crate::{format_module, format_node}; + use crate::{format_module, format_node, PyFormatOptions}; use anyhow::Result; use insta::assert_snapshot; use ruff_python_ast::source_code::CommentRangesBuilder; @@ -284,7 +241,9 @@ if True: pass # trailing "#; - let actual = format_module(input)?.as_code().to_string(); + let actual = format_module(input, PyFormatOptions::default())? + .as_code() + .to_string(); assert_eq!(expected, actual); Ok(()) } @@ -315,7 +274,13 @@ if [ // Parse the AST. let python_ast = parse_tokens(tokens, Mode::Module, "").unwrap(); - let formatted = format_node(&python_ast, &comment_ranges, src).unwrap(); + let formatted = format_node( + &python_ast, + &comment_ranges, + src, + PyFormatOptions::default(), + ) + .unwrap(); // Uncomment the `dbg` to print the IR. // Use `dbg_write!(f, []) instead of `write!(f, [])` in your formatting code to print some IR diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs new file mode 100644 index 0000000000..e7302a69df --- /dev/null +++ b/crates/ruff_python_formatter/src/options.rs @@ -0,0 +1,118 @@ +use ruff_formatter::printer::{LineEnding, PrinterOptions}; +use ruff_formatter::{FormatOptions, IndentStyle, LineWidth}; + +#[derive(Clone, Debug)] +pub struct PyFormatOptions { + /// Specifies the indent style: + /// * Either a tab + /// * or a specific amount of spaces + indent_style: IndentStyle, + + /// The preferred line width at which the formatter should wrap lines. + line_width: LineWidth, + + /// 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, +} + +impl PyFormatOptions { + pub fn magic_trailing_comma(&self) -> MagicTrailingComma { + self.magic_trailing_comma + } + + pub fn quote_style(&self) -> QuoteStyle { + self.quote_style + } + + pub fn with_quote_style(&mut self, style: QuoteStyle) -> &mut Self { + self.quote_style = style; + self + } + + pub fn with_magic_trailing_comma(&mut self, trailing_comma: MagicTrailingComma) -> &mut Self { + self.magic_trailing_comma = trailing_comma; + self + } +} + +impl FormatOptions for PyFormatOptions { + fn indent_style(&self) -> IndentStyle { + self.indent_style + } + + fn line_width(&self) -> LineWidth { + self.line_width + } + + fn as_print_options(&self) -> PrinterOptions { + PrinterOptions { + tab_width: 4, + print_width: self.line_width.into(), + line_ending: LineEnding::LineFeed, + indent_style: self.indent_style, + } + } +} + +impl Default for PyFormatOptions { + fn default() -> Self { + Self { + indent_style: IndentStyle::Space(4), + line_width: LineWidth::try_from(88).unwrap(), + quote_style: QuoteStyle::default(), + magic_trailing_comma: MagicTrailingComma::default(), + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum QuoteStyle { + Single, + #[default] + Double, +} + +impl QuoteStyle { + pub const fn as_char(self) -> char { + match self { + QuoteStyle::Single => '\'', + QuoteStyle::Double => '"', + } + } + + #[must_use] + pub const fn opposite(self) -> QuoteStyle { + match self { + QuoteStyle::Single => QuoteStyle::Double, + QuoteStyle::Double => QuoteStyle::Single, + } + } +} + +impl TryFrom for QuoteStyle { + type Error = (); + + fn try_from(value: char) -> std::result::Result { + match value { + '\'' => Ok(QuoteStyle::Single), + '"' => Ok(QuoteStyle::Double), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone, Debug, Default)] +pub enum MagicTrailingComma { + #[default] + Preserve, + Skip, +} + +impl MagicTrailingComma { + pub const fn is_preserve(self) -> bool { + matches!(self, Self::Preserve) + } +} diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 85329fba8c..d16576a6db 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -188,7 +188,8 @@ mod tests { use crate::comments::Comments; use crate::prelude::*; use crate::statement::suite::SuiteLevel; - use ruff_formatter::{format, IndentStyle, SimpleFormatOptions}; + use crate::PyFormatOptions; + use ruff_formatter::format; use rustpython_parser::ast::Suite; use rustpython_parser::Parse; @@ -216,14 +217,7 @@ def trailing_func(): let statements = Suite::parse(source, "test.py").unwrap(); - let context = PyFormatContext::new( - SimpleFormatOptions { - indent_style: IndentStyle::Space(4), - ..SimpleFormatOptions::default() - }, - source, - Comments::default(), - ); + let context = PyFormatContext::new(PyFormatOptions::default(), source, Comments::default()); let test_formatter = format_with(|f: &mut PyFormatter| statements.format().with_options(level).fmt(f)); diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index edfc112a86..34e41229a9 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -1,4 +1,4 @@ -use ruff_python_formatter::format_module; +use ruff_python_formatter::{format_module, PyFormatOptions}; use similar::TextDiff; use std::fmt::{Formatter, Write}; use std::fs; @@ -9,7 +9,8 @@ fn black_compatibility() { let test_file = |input_path: &Path| { let content = fs::read_to_string(input_path).unwrap(); - let printed = format_module(&content).expect("Formatting to succeed"); + let printed = + format_module(&content, PyFormatOptions::default()).expect("Formatting to succeed"); let expected_path = input_path.with_extension("py.expect"); let expected_output = fs::read_to_string(&expected_path) @@ -88,7 +89,8 @@ fn format() { let test_file = |input_path: &Path| { let content = fs::read_to_string(input_path).unwrap(); - let printed = format_module(&content).expect("Formatting to succeed"); + let printed = + format_module(&content, PyFormatOptions::default()).expect("Formatting to succeed"); let formatted_code = printed.as_code(); ensure_stability_when_formatting_twice(formatted_code); @@ -117,7 +119,7 @@ fn format() { /// Format another time and make sure that there are no changes anymore fn ensure_stability_when_formatting_twice(formatted_code: &str) { - let reformatted = match format_module(formatted_code) { + let reformatted = match format_module(formatted_code, PyFormatOptions::default()) { Ok(reformatted) => reformatted, Err(err) => { panic!(