Fix codeblock dynamic line length calculation for indented examples (#13523)

This commit is contained in:
Micha Reiser 2024-09-27 09:09:07 +02:00 committed by GitHub
parent 7706f561a9
commit c046101b79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1276 additions and 20 deletions

View file

@ -44,3 +44,12 @@ pub(crate) fn is_empty_parameters_no_unnecessary_parentheses_around_return_value
pub(crate) fn is_match_case_parentheses_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}
/// This preview style fixes a bug with the docstring's `line-length` calculation when using the `dynamic` mode.
/// The new style now respects the indent **inside** the docstring and reduces the `line-length` accordingly
/// so that the docstring's code block fits into the global line-length setting.
pub(crate) fn is_docstring_code_block_in_docstring_indent_enabled(
context: &PyFormatContext,
) -> bool {
context.is_preview()
}

View file

@ -18,11 +18,11 @@ use {
ruff_text_size::{Ranged, TextLen, TextRange, TextSize},
};
use super::NormalizedString;
use crate::preview::is_docstring_code_block_in_docstring_indent_enabled;
use crate::string::StringQuotes;
use crate::{prelude::*, DocstringCodeLineWidth, FormatModuleError};
use super::NormalizedString;
/// Format a docstring by trimming whitespace and adjusting the indentation.
///
/// Summary of changes we make:
@ -189,7 +189,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
// We don't want to count whitespace-only lines as miss-indented
.filter(|line| !line.trim().is_empty())
.map(Indentation::from_str)
.min_by_key(|indentation| indentation.width())
.min_by_key(|indentation| indentation.columns())
.unwrap_or_default();
DocstringLinePrinter {
@ -353,7 +353,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
};
// This looks suspicious, but it's consistent with the whitespace
// normalization that will occur anyway.
let indent = " ".repeat(min_indent.width());
let indent = " ".repeat(min_indent.columns());
for docline in formatted_lines {
self.print_one(
&docline.map(|line| std::format!("{indent}{line}")),
@ -363,7 +363,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
CodeExampleKind::Markdown(fenced) => {
// This looks suspicious, but it's consistent with the whitespace
// normalization that will occur anyway.
let indent = " ".repeat(fenced.opening_fence_indent.width());
let indent = " ".repeat(fenced.opening_fence_indent.columns());
for docline in formatted_lines {
self.print_one(
&docline.map(|line| std::format!("{indent}{line}")),
@ -455,7 +455,7 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
// (see example in [`format_docstring`] doc comment). We then
// prepend the in-docstring indentation to the string.
let indent_len =
Indentation::from_str(trim_end).width() - self.stripped_indentation.width();
Indentation::from_str(trim_end).columns() - self.stripped_indentation.columns();
let in_docstring_indent = " ".repeat(indent_len) + trim_end.trim_start();
text(&in_docstring_indent).fmt(self.f)?;
};
@ -500,11 +500,24 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
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
let mut current_indent = indent_level
.to_ascii_spaces(indent_width)
.saturating_add(kind.extra_indent_ascii_spaces());
if is_docstring_code_block_in_docstring_indent_enabled(self.f.context()) {
// Add the in-docstring indentation
current_indent = current_indent.saturating_add(
u16::try_from(
kind.indent()
.columns()
.saturating_sub(self.stripped_indentation.columns()),
)
.unwrap_or(u16::MAX),
);
}
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")
LineWidth::try_from(width).expect("width should be capped at a minimum of 1")
}
};
@ -828,6 +841,26 @@ impl<'src> CodeExampleKind<'src> {
_ => 0,
}
}
/// The indent of the entire code block relative to the start of the line.
///
/// For example:
/// ```python
/// def test():
/// """Docstring
/// Example:
/// >>> 1 + 1
/// ```
///
/// The `>>> ` block has an indent of 8 columns: The shared indent with the docstring and the 4 spaces
/// inside the docstring.
fn indent(&self) -> Indentation {
match self {
CodeExampleKind::Doctest(doctest) => Indentation::from_str(doctest.ps1_indent),
CodeExampleKind::Rst(rst) => rst.min_indent.unwrap_or(rst.opening_indent),
CodeExampleKind::Markdown(markdown) => markdown.opening_fence_indent,
}
}
}
/// State corresponding to a single doctest code example found in a docstring.
@ -1663,7 +1696,7 @@ impl Indentation {
/// to the next multiple of 8. This is effectively a port of
/// [`str.expandtabs`](https://docs.python.org/3/library/stdtypes.html#str.expandtabs),
/// which black [calls with the default tab width of 8](https://github.com/psf/black/blob/c36e468794f9256d5e922c399240d49782ba04f1/src/black/strings.py#L61).
const fn width(self) -> usize {
const fn columns(self) -> usize {
match self {
Self::Spaces(count) => count,
Self::Tabs(count) => count * Self::TAB_INDENT_WIDTH,
@ -1769,7 +1802,7 @@ impl Indentation {
fn trim_start_str(self, line: &str) -> &str {
let mut seen_indent_len = 0;
let mut trimmed = line;
let indent_len = self.width();
let indent_len = self.columns();
for char in line.chars() {
if seen_indent_len >= indent_len {
@ -1797,13 +1830,13 @@ impl Indentation {
impl PartialOrd for Indentation {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.width().cmp(&other.width()))
Some(self.columns().cmp(&other.columns()))
}
}
impl PartialEq for Indentation {
fn eq(&self, other: &Self) -> bool {
self.width() == other.width()
self.columns() == other.columns()
}
}
@ -1843,10 +1876,10 @@ mod tests {
use crate::string::docstring::Indentation;
#[test]
fn test_indentation_like_black() {
assert_eq!(Indentation::from_str("\t \t \t").width(), 24);
assert_eq!(Indentation::from_str("\t \t").width(), 24);
assert_eq!(Indentation::from_str("\t\t\t").width(), 24);
assert_eq!(Indentation::from_str(" ").width(), 4);
fn indentation_like_black() {
assert_eq!(Indentation::from_str("\t \t \t").columns(), 24);
assert_eq!(Indentation::from_str("\t \t").columns(), 24);
assert_eq!(Indentation::from_str("\t\t\t").columns(), 24);
assert_eq!(Indentation::from_str(" ").columns(), 4);
}
}