Create PyFormatOptions

<!--
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR adds a new `PyFormatOptions` struct that stores the python formatter options. 
The new options aren't used yet, with the exception of magical trailing commas and the options passed to the printer. 
I'll follow up with more PRs that use the new options (e.g. `QuoteStyle`).

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

`cargo test` I'll follow up with a new PR that adds support for overriding the options in our fixture tests.
This commit is contained in:
Micha Reiser 2023-06-26 14:02:17 +02:00 committed by GitHub
parent a52cd47c7f
commit dd0d1afb66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 170 additions and 88 deletions

View file

@ -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")
});
},
);
}

View file

@ -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<ExitStatus> {
// 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 {

View file

@ -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<anyhow::Error> 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 {

View file

@ -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());

View file

@ -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<String> {
let python_ast = parse_tokens(tokens, Mode::Module, "<filename>")
.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)));
}

View file

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

View file

@ -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<Printed> {
pub fn format_module(contents: &str, options: PyFormatOptions) -> Result<Printed> {
// Tokenize once
let mut tokens = Vec::new();
let mut comment_ranges = CommentRangesBuilder::default();
@ -107,7 +105,7 @@ pub fn format_module(contents: &str) -> Result<Printed> {
let python_ast = parse_tokens(tokens, Mode::Module, "<filename>")
.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<Formatted<PyFormatContext<'a>>> {
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<PyFormatContext<'_>> 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<char> for QuoteStyle {
type Error = ();
fn try_from(value: char) -> std::result::Result<Self, Self::Error> {
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, "<filename>").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

View file

@ -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<char> for QuoteStyle {
type Error = ();
fn try_from(value: char) -> std::result::Result<Self, Self::Error> {
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)
}
}

View file

@ -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));

View file

@ -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!(