ruff_python_formatter: fix 'dynamic' mode with doctests (#9129)

This fixes a bug where the current indent level was not calculated
correctly for doctests. Namely, it didn't account for the extra indent
level (in terms of ASCII spaces) used by by the PS1 (`>>> `) and PS2
(`... `) prompts. As a result, lines could extend up to 4 spaces beyond
the configured line length limit.

We fix that by passing the `CodeExampleKind` to the `format` routine
instead of just the code itself. In this way, `format` can query whether
there will be any extra indent added _after_ formatting the code and
take that into account for its line length setting.

We add a few regression tests, taken directly from @stinodego's
examples.

Fixes #9126
This commit is contained in:
Andrew Gallant 2023-12-14 09:53:43 -05:00 committed by GitHub
parent c99eae2c08
commit 28b1aa201b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 327 additions and 14 deletions

View file

@ -316,7 +316,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
}
}
CodeExampleAddAction::Format { mut kind } => {
let Some(formatted_lines) = self.format(kind.code())? else {
let Some(formatted_lines) = self.format(&mut kind)? else {
// Since we've failed to emit these lines, we need to
// put them back in the queue but have them jump to the
// front of the queue to get processed before any other
@ -432,9 +432,13 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
Ok(())
}
/// Given a sequence of lines from a code snippet, format them and return
/// Given a code example, format them and return
/// the formatted code as a sequence of owned docstring lines.
///
/// This may mutate the code example in place if extracting the lines of
/// code requires adjusting which part of each line is used for the actual
/// code bit.
///
/// This routine generally only returns an error when the recursive call
/// to the formatter itself returns a `FormatError`. In all other cases
/// (for example, if the code snippet is invalid Python or even if the
@ -448,10 +452,25 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
/// but at time of writing, it wasn't clear to me how to best do that.
fn format(
&mut self,
code: &[CodeExampleLine<'_>],
kind: &mut CodeExampleKind<'_>,
) -> FormatResult<Option<Vec<OutputDocstringLine<'static>>>> {
use ruff_python_parser::AsMode;
let line_width = match self.f.options().docstring_code_line_width() {
DocstringCodeLineWidth::Fixed(width) => width,
DocstringCodeLineWidth::Dynamic => {
let global_line_width = self.f.options().line_width().value();
let indent_width = self.f.options().indent_width();
let indent_level = self.f.context().indent_level();
let current_indent = indent_level
.to_ascii_spaces(indent_width)
.saturating_add(kind.extra_indent_ascii_spaces());
let width = std::cmp::max(1, global_line_width.saturating_sub(current_indent));
LineWidth::try_from(width).expect("width is capped at a minimum of 1")
}
};
let code = kind.code();
let (Some(unformatted_first), Some(unformatted_last)) = (code.first(), code.last()) else {
return Ok(None);
};
@ -460,17 +479,6 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
.map(|line| line.code)
.collect::<Vec<&str>>()
.join("\n");
let line_width = match self.f.options().docstring_code_line_width() {
DocstringCodeLineWidth::Fixed(width) => width,
DocstringCodeLineWidth::Dynamic => {
let global_line_width = self.f.options().line_width().value();
let indent_width = self.f.options().indent_width();
let indent_level = self.f.context().indent_level();
let current_indent = indent_level.to_ascii_spaces(indent_width);
let width = std::cmp::max(1, global_line_width.saturating_sub(current_indent));
LineWidth::try_from(width).expect("width is capped at a minimum of 1")
}
};
let options = self
.f
.options()
@ -778,6 +786,17 @@ impl<'src> CodeExampleKind<'src> {
CodeExampleKind::Markdown(fenced) => fenced.lines,
}
}
/// This returns any extra indent that will be added after formatting this
/// code example.
///
/// The extra indent is expressed in units of ASCII space characters.
fn extra_indent_ascii_spaces(&self) -> u16 {
match *self {
CodeExampleKind::Doctest(_) => 4,
_ => 0,
}
}
}
/// State corresponding to a single doctest code example found in a docstring.