diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.options.json new file mode 100644 index 0000000000..4a3e3c6dd7 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.options.json @@ -0,0 +1,15 @@ +[ + { + "indent_style": { + "Space": 4 + } + }, + { + "indent_style": { + "Space": 2 + } + }, + { + "indent_style": "Tab" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py new file mode 100644 index 0000000000..60c655f686 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py @@ -0,0 +1,102 @@ +def single_line_backslashes1(): + """ content\ """ + return + + +def single_line_backslashes2(): + """ content\\ """ + return + + +def single_line_backslashes3(): + """ content\\\ """ + return + + +def multiline_backslashes1(): + """This is a docstring with + some lines of text\ """ + return + + +def multiline_backslashes2(): + """This is a docstring with + some lines of text\\ """ + return + + +def multiline_backslashes3(): + """This is a docstring with + some lines of text\\\ """ + return + + +def multiple_negatively_indented_docstring_lines(): + """a + b + c + d + e + """ + + +def overindentend_docstring(): + """a + over-indented + """ + + +def comment_before_docstring(): + # don't lose this function comment ... + """Does nothing. + + But it has comments + """ # ... neither lose this function comment + + +class CommentBeforeDocstring(): + # don't lose this class comment ... + """Empty class. + + But it has comments + """ # ... neither lose this class comment + + +class IndentMeSome: + def doc_string_without_linebreak_after_colon(self): """ This is somewhat strange + a + b + We format this a is the docstring had started properly indented on the next + line if the target indentation. This may we incorrect since source and target + indentation can be incorrect, but this is also an edge case. + """ + + +class IgnoreImplicitlyConcatenatedStrings: + """""" "" + + +def docstring_that_ends_with_quote_and_a_line_break1(): + """ + he said "the news of my death have been greatly exaggerated" + """ + + +def docstring_that_ends_with_quote_and_a_line_break2(): + """he said "the news of my death have been greatly exaggerated" + """ + + +def docstring_that_ends_with_quote_and_a_line_break3(): + """he said "the news of my death have been greatly exaggerated" + + """ + + +class TabbedIndent: + def tabbed_indent(self): + """check for correct tabbed formatting + ^^^^^^^^^^ + Normal indented line + - autor + """ diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index e147eb8cd5..3df7f7650b 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -1,16 +1,16 @@ use std::borrow::Cow; use bitflags::bitflags; + +use ruff_formatter::{format_args, write, FormatError}; use ruff_python_ast::node::AnyNodeRef; +use ruff_python_ast::str::is_implicit_concatenation; use ruff_python_ast::{self as ast, ExprConstant, ExprFString, Ranged}; use ruff_python_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType}; use ruff_python_parser::{Mode, Tok}; use ruff_source_file::Locator; use ruff_text_size::{TextLen, TextRange, TextSize}; -use ruff_formatter::{format_args, write, FormatError}; -use ruff_python_ast::str::is_implicit_concatenation; - use crate::comments::{leading_comments, trailing_comments}; use crate::expression::parentheses::{ in_parentheses_only_group, in_parentheses_only_soft_line_break_or_space, @@ -78,7 +78,7 @@ pub(super) struct FormatString<'a> { pub enum StringLayout { #[default] Default, - + DocString, ImplicitConcatenatedBinaryLeftSide, } @@ -109,10 +109,25 @@ impl<'a> Format> for FormatString<'a> { if is_implicit_concatenation(string_content) { in_parentheses_only_group(&FormatStringContinuation::new(self.string)).fmt(f) } else { - FormatStringPart::new(string_range, self.string.quoting(&f.context().locator())) - .fmt(f) + FormatStringPart::new( + string_range, + self.string.quoting(&f.context().locator()), + &f.context().locator(), + f.options().quote_style(), + ) + .fmt(f) } } + StringLayout::DocString => { + let string_part = FormatStringPart::new( + self.string.range(), + // f-strings can't be docstrings + Quoting::CanChange, + &f.context().locator(), + f.options().quote_style(), + ); + format_docstring(&string_part, f) + } StringLayout::ImplicitConcatenatedBinaryLeftSide => { FormatStringContinuation::new(self.string).fmt(f) } @@ -137,6 +152,7 @@ impl Format> for FormatStringContinuation<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { let comments = f.context().comments().clone(); let locator = f.context().locator(); + let quote_style = f.options().quote_style(); let mut dangling_comments = comments.dangling_comments(self.string); let string_range = self.string.range(); @@ -213,7 +229,12 @@ impl Format> for FormatStringContinuation<'_> { joiner.entry(&format_args![ line_suffix_boundary(), leading_comments(leading_part_comments), - FormatStringPart::new(token_range, self.string.quoting(&locator)), + FormatStringPart::new( + token_range, + self.string.quoting(&locator), + &locator, + quote_style, + ), trailing_comments(trailing_part_comments) ]); @@ -235,63 +256,67 @@ impl Format> for FormatStringContinuation<'_> { } struct FormatStringPart { - part_range: TextRange, - quoting: Quoting, + prefix: StringPrefix, + preferred_quotes: StringQuotes, + range: TextRange, + is_raw_string: bool, } impl FormatStringPart { - const fn new(range: TextRange, quoting: Quoting) -> Self { + fn new(range: TextRange, quoting: Quoting, locator: &Locator, quote_style: QuoteStyle) -> Self { + let string_content = locator.slice(range); + + let prefix = StringPrefix::parse(string_content); + let after_prefix = &string_content[usize::from(prefix.text_len())..]; + + let quotes = + StringQuotes::parse(after_prefix).expect("Didn't find string quotes after prefix"); + let relative_raw_content_range = TextRange::new( + prefix.text_len() + quotes.text_len(), + string_content.text_len() - quotes.text_len(), + ); + let raw_content_range = relative_raw_content_range + range.start(); + + let raw_content = &string_content[relative_raw_content_range]; + let is_raw_string = prefix.is_raw_string(); + let preferred_quotes = match quoting { + Quoting::Preserve => quotes, + Quoting::CanChange => { + if is_raw_string { + preferred_quotes_raw(raw_content, quotes, quote_style) + } else { + preferred_quotes(raw_content, quotes, quote_style) + } + } + }; + Self { - part_range: range, - quoting, + prefix, + range: raw_content_range, + preferred_quotes, + is_raw_string, } } } impl Format> for FormatStringPart { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - let string_content = f.context().locator().slice(self.part_range); - - let prefix = StringPrefix::parse(string_content); - let after_prefix = &string_content[usize::from(prefix.text_len())..]; - - let quotes = StringQuotes::parse(after_prefix).ok_or(FormatError::syntax_error( - "Didn't find string quotes after prefix", - ))?; - let relative_raw_content_range = TextRange::new( - prefix.text_len() + quotes.text_len(), - string_content.text_len() - quotes.text_len(), + let (normalized, contains_newlines) = normalize_string( + f.context().locator().slice(self.range), + self.preferred_quotes, + self.is_raw_string, ); - let raw_content_range = relative_raw_content_range + self.part_range.start(); - - let raw_content = &string_content[relative_raw_content_range]; - let is_raw_string = prefix.is_raw_string(); - let preferred_quotes = match self.quoting { - Quoting::Preserve => quotes, - Quoting::CanChange => { - if is_raw_string { - preferred_quotes_raw(raw_content, quotes, f.options().quote_style()) - } else { - preferred_quotes(raw_content, quotes, f.options().quote_style()) - } - } - }; - - write!(f, [prefix, preferred_quotes])?; - - let (normalized, contains_newlines) = - normalize_string(raw_content, preferred_quotes, is_raw_string); + write!(f, [self.prefix, self.preferred_quotes])?; match normalized { Cow::Borrowed(_) => { - source_text_slice(raw_content_range, contains_newlines).fmt(f)?; + source_text_slice(self.range, contains_newlines).fmt(f)?; } Cow::Owned(normalized) => { - dynamic_text(&normalized, Some(raw_content_range.start())).fmt(f)?; + dynamic_text(&normalized, Some(self.range.start())).fmt(f)?; } } - - preferred_quotes.fmt(f) + self.preferred_quotes.fmt(f) } } @@ -639,3 +664,311 @@ fn normalize_string( (normalized, newlines) } + +/// For docstring indentation, black counts spaces as 1 and tabs by increasing the indentation up +/// 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) +fn count_indentation_like_black(line: &str) -> TextSize { + let tab_width: u32 = 8; + let mut indentation = TextSize::default(); + for char in line.chars() { + if char == '\t' { + // Pad to the next multiple of tab_width + indentation += TextSize::from(tab_width - (indentation.to_u32().rem_euclid(tab_width))); + } else if char.is_whitespace() { + indentation += char.text_len(); + } else { + return indentation; + } + } + indentation +} + +/// Format a docstring by trimming whitespace and adjusting the indentation. +/// +/// Summary of changes we make: +/// * Normalize the string like all other strings +/// * Ignore docstring that have an escaped newline +/// * Trim all trailing whitespace, except for a chaperone space that avoids quotes or backslashes +/// in the last line. +/// * Trim leading whitespace on the first line, again except for a chaperone space +/// * If there is only content in the first line and after that only whitespace, collapse the +/// docstring into one line +/// * Adjust the indentation (see below) +/// +/// # Docstring indentation +/// +/// Unlike any other string, like black we change the indentation of docstring lines. +/// +/// We want to preserve the indentation inside the docstring relative to the suite statement/block +/// indent that the docstring statement is in, but also want to apply the change of the outer +/// indentation in the docstring, e.g. +/// ```python +/// def sparkle_sky(): +/// """Make a pretty sparkly sky. +/// * * ✨ *. . +/// * * ✨ . +/// . * . ✨ * . . +/// """ +/// ``` +/// should become +/// ```python +/// def sparkle_sky(): +/// """Make a pretty sparkly sky. +/// * * ✨ *. . +/// * * ✨ . +/// . * . ✨ * . . +/// """ +/// ``` +/// We can't compute the full indentation here since we don't know what the block indent of +/// the doc comment will be yet and which we can only have added by formatting each line +/// separately with a hard line break. This means we need to strip shared indentation from +/// docstring while preserving the in-docstring bigger-than-suite-statement indentation. Example: +/// ```python +/// def f(): +/// """first line +/// line a +/// line b +/// """ +/// ``` +/// The docstring indentation is 2, the block indents will change this to 4 (but we can't +/// determine this at this point). The indentation of line a is 2, so we trim ` line a` +/// to `line a`. For line b it's 5, so we trim it to `line b` and pad with 5-2=3 spaces to +/// ` line b`. The closing quotes, being on their own line, are stripped get only the +/// default indentation. Fully formatted: +/// ```python +/// def f(): +/// """first line +/// line a +/// line b +/// """ +/// ``` +/// +/// Tabs are counted by padding them to the next multiple of 8 according to +/// [`str.expandtabs`](https://docs.python.org/3/library/stdtypes.html#str.expandtabs). When +/// we see indentation that contains a tab or any other none ascii-space whitespace we rewrite the +/// string. +/// +/// Additionally, if any line in the docstring has less indentation than the docstring +/// (effectively a negative indentation wrt. to the current level), we pad all lines to the +/// level of the docstring with spaces. +/// ```python +/// def f(): +/// """first line +/// line a +/// line b +/// line c +/// """ +/// ``` +/// Here line a is 3 columns negatively indented, so we pad all lines by an extra 3 spaces: +/// ```python +/// def f(): +/// """first line +/// line a +/// line b +/// line c +/// """ +/// ``` +fn format_docstring(string_part: &FormatStringPart, f: &mut PyFormatter) -> FormatResult<()> { + let locator = f.context().locator(); + + // Black doesn't change the indentation of docstrings that contain an escaped newline + if locator.slice(string_part.range).contains("\\\n") { + return string_part.fmt(f); + } + + let (normalized, _) = normalize_string( + locator.slice(string_part.range), + string_part.preferred_quotes, + string_part.is_raw_string, + ); + // is_borrowed is unstable :/ + let already_normalized = matches!(normalized, Cow::Borrowed(_)); + + let mut lines = normalized.lines().peekable(); + + // Start the string + write!( + f, + [ + source_position(string_part.range.start()), + string_part.prefix, + string_part.preferred_quotes + ] + )?; + // We track where in the source docstring we are (in source code byte offsets) + let mut offset = string_part.range.start(); + + // The first line directly after the opening quotes has different rules than the rest, mainly + // that we remove all leading whitespace as there's no indentation + let first = lines.next().unwrap_or_default(); + // Black trims whitespace using [`str.strip()`](https://docs.python.org/3/library/stdtypes.html#str.strip) + // https://github.com/psf/black/blob/b4dca26c7d93f930bbd5a7b552807370b60d4298/src/black/strings.py#L77-L85 + // So we use the unicode whitespace definition through `trim_{start,end}` instead of the python + // tokenizer whitespace definition in `trim_whitespace_{start,end}`. + let trim_end = first.trim_end(); + let trim_both = trim_end.trim_start(); + + // Edge case: The first line is `""" "content`, so we need to insert chaperone space that keep + // inner quotes and closing quotes from getting to close to avoid `""""content` + if trim_both.starts_with(string_part.preferred_quotes.style.as_char()) { + space().fmt(f)?; + } + + if !trim_end.is_empty() { + // For the first line of the docstring we strip the leading and trailing whitespace, e.g. + // `""" content ` to `"""content` + let leading_whitespace = trim_end.text_len() - trim_both.text_len(); + let trimmed_line_range = + TextRange::at(offset, trim_end.text_len()).add_start(leading_whitespace); + if already_normalized { + source_text_slice(trimmed_line_range, ContainsNewlines::No).fmt(f)?; + } else { + dynamic_text(trim_both, Some(trimmed_line_range.start())).fmt(f)?; + } + } + offset += first.text_len(); + + // Check if we have a single line (or empty) docstring + if normalized[first.len()..].trim().is_empty() { + // For `"""\n"""` or other whitespace between the quotes, black keeps a single whitespace, + // but `""""""` doesn't get one inserted. + if needs_chaperone_space(string_part, trim_end) + || (trim_end.is_empty() && !normalized.is_empty()) + { + space().fmt(f)?; + } + string_part.preferred_quotes.fmt(f)?; + return Ok(()); + } + + hard_line_break().fmt(f)?; + // We know that the normalized string has \n line endings + offset += "\n".text_len(); + + // If some line of the docstring is less indented than the function body, we pad all lines to + // align it with the docstring statement. Conversely, if all lines are over-indented, we strip + // the extra indentation. We call this stripped indentation since it's relative to the block + // indent printer-made indentation. + let stripped_indentation = lines + .clone() + // We don't want to count whitespace-only lines as miss-indented + .filter(|line| !line.trim().is_empty()) + .map(count_indentation_like_black) + .min() + .unwrap_or_default(); + + while let Some(line) = lines.next() { + let is_last = lines.peek().is_none(); + format_docstring_line( + line, + is_last, + offset, + stripped_indentation, + already_normalized, + f, + )?; + // We know that the normalized string has \n line endings + offset += line.text_len() + "\n".text_len(); + } + + // Same special case in the last line as for the first line + let trim_end = normalized + .as_ref() + .trim_end_matches(|c: char| c.is_whitespace() && c != '\n'); + if needs_chaperone_space(string_part, trim_end) { + space().fmt(f)?; + } + + write!( + f, + [ + string_part.preferred_quotes, + source_position(string_part.range.end()) + ] + ) +} + +/// If the last line of the docstring is `content" """` or `content\ """`, we need a chaperone space +/// that avoids `content""""` and `content\"""`. This does only applies to un-escaped backslashes, +/// so `content\\ """` doesn't need a space while `content\\\ """` does. +fn needs_chaperone_space(string_part: &FormatStringPart, trim_end: &str) -> bool { + trim_end.ends_with(string_part.preferred_quotes.style.as_char()) + || trim_end.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 +} + +/// Format a docstring line that is not the first line +fn format_docstring_line( + line: &str, + is_last: bool, + offset: TextSize, + stripped_indentation: TextSize, + already_normalized: bool, + f: &mut PyFormatter, +) -> FormatResult<()> { + let trim_end = line.trim_end(); + if trim_end.is_empty() { + return if is_last { + // If the doc string ends with ` """`, the last line is ` `, but we don't want to + // insert an empty line (but close the docstring) + Ok(()) + } else { + empty_line().fmt(f) + }; + } + + let tab_or_non_ascii_space = trim_end + .chars() + .take_while(|c| c.is_whitespace()) + .any(|c| c != ' '); + + if tab_or_non_ascii_space { + // We strip the indentation that is shared with the docstring statement, unless a line + // was indented less than the docstring statement, in which we strip only this much + // indentation to implicitly pad all lines by the difference, or all lines were + // overindented, in which case we strip the additional whitespace (see example in + // [`format_docstring`] doc comment). We then prepend the in-docstring indentation to the + // string. + let indent_len = count_indentation_like_black(trim_end) - stripped_indentation; + let in_docstring_indent = " ".repeat(indent_len.to_usize()) + trim_end.trim_start(); + dynamic_text(&in_docstring_indent, Some(offset)).fmt(f)?; + } else { + // Take the string with the trailing whitespace removed, then also skip the leading + // whitespace + let trimmed_line_range = + TextRange::at(offset, trim_end.text_len()).add_start(stripped_indentation); + if already_normalized { + source_text_slice(trimmed_line_range, ContainsNewlines::No).fmt(f)?; + } else { + // All indents are ascii spaces, so the slicing is correct + dynamic_text( + &trim_end[stripped_indentation.to_usize()..], + Some(trimmed_line_range.start()), + ) + .fmt(f)?; + } + } + + // We handled the case that the closing quotes are on their own line above (the last line is + // empty except for whitespace). If they are on the same line as content, we don't insert a line + // break. + if !is_last { + hard_line_break().fmt(f)?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::expression::string::count_indentation_like_black; + + #[test] + fn test_indentation_like_black() { + assert_eq!(count_indentation_like_black("\t \t \t").to_u32(), 24); + assert_eq!(count_indentation_like_black("\t \t").to_u32(), 24); + assert_eq!(count_indentation_like_black("\t\t\t").to_u32(), 24); + assert_eq!(count_indentation_like_black(" ").to_u32(), 4); + } +} diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index ad3da3c927..02a9a8180b 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -1,9 +1,15 @@ use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions}; use ruff_python_ast::helpers::is_compound_statement; -use ruff_python_ast::{self as ast, Constant, Expr, Ranged, Stmt, Suite}; +use ruff_python_ast::str::is_implicit_concatenation; +use ruff_python_ast::{self as ast, Expr, Ranged, Stmt, Suite}; +use ruff_python_ast::{Constant, ExprConstant}; use ruff_python_trivia::{lines_after_ignoring_trivia, lines_before}; +use ruff_source_file::Locator; +use crate::comments::{leading_comments, trailing_comments}; use crate::context::{NodeLevel, WithNodeLevel}; +use crate::expression::expr_constant::ExprConstantLayout; +use crate::expression::string::StringLayout; use crate::prelude::*; /// Level at which the [`Suite`] appears in the source code. @@ -71,44 +77,76 @@ impl FormatRule> for FormatSuite { } write!(f, [first.format()])?; } - SuiteKind::Class if is_docstring(first) => { - if !comments.has_leading_comments(first) && lines_before(first.start(), source) > 1 - { - // Allow up to one empty line before a class docstring, e.g., this is - // stable formatting: + SuiteKind::Function => { + if let Some(constant) = get_docstring(first, &f.context().locator()) { + write!( + f, + [ + // We format the expression, but the statement carries the comments + leading_comments(comments.leading_comments(first)), + constant + .format() + .with_options(ExprConstantLayout::String(StringLayout::DocString)), + trailing_comments(comments.trailing_comments(first)), + ] + )?; + } else { + write!(f, [first.format()])?; + } + } + SuiteKind::Class => { + if let Some(constant) = get_docstring(first, &f.context().locator()) { + if !comments.has_leading_comments(first) + && lines_before(first.start(), source) > 1 + { + // Allow up to one empty line before a class docstring + // ```python + // class Test: + // + // """Docstring""" + // ``` + write!(f, [empty_line()])?; + } + write!( + f, + [ + // We format the expression, but the statement carries the comments + leading_comments(comments.leading_comments(first)), + constant + .format() + .with_options(ExprConstantLayout::String(StringLayout::DocString)), + trailing_comments(comments.trailing_comments(first)), + ] + )?; + + // Enforce an empty line after a class docstring // ```python // class Test: + // """Docstring""" + // + // ... + // + // + // class Test: // // """Docstring""" + // + // ... // ``` - write!(f, [empty_line()])?; - } - write!(f, [first.format()])?; - - // Enforce an empty line after a class docstring, e.g., these are both stable - // formatting: - // ```python - // class Test: - // """Docstring""" - // - // ... - // - // - // class Test: - // - // """Docstring""" - // - // ... - // ``` - if let Some(second) = iter.next() { - // Format the subsequent statement immediately. This rule takes precedence - // over the rules in the loop below (and most of them won't apply anyway, - // e.g., we know the first statement isn't an import). - write!(f, [empty_line(), second.format()])?; - last = second; + // Unlike black, we add the newline also after single quoted docstrings + if let Some(second) = iter.next() { + // Format the subsequent statement immediately. This rule takes precedence + // over the rules in the loop below (and most of them won't apply anyway, + // e.g., we know the first statement isn't an import). + write!(f, [empty_line(), second.format()])?; + last = second; + } + } else { + // No docstring, use normal formatting + write!(f, [first.format()])?; } } - _ => { + SuiteKind::TopLevel => { write!(f, [first.format()])?; } } @@ -218,18 +256,27 @@ const fn is_import_definition(stmt: &Stmt) -> bool { matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) } -fn is_docstring(stmt: &Stmt) -> bool { +/// Checks if the statement is a simple string that can be formatted as a docstring +fn get_docstring<'a>(stmt: &'a Stmt, locator: &Locator) -> Option<&'a ExprConstant> { let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { - return false; + return None; }; - matches!( - value.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Str(..), - .. - }) - ) + let Expr::Constant(constant) = value.as_ref() else { + return None; + }; + if let ExprConstant { + value: Constant::Str(..), + range, + .. + } = constant + { + if is_implicit_concatenation(locator.slice(*range)) { + return None; + } + return Some(constant); + } + None } impl FormatRuleWithOptions> for FormatSuite { @@ -260,7 +307,6 @@ impl<'ast> IntoFormat> for Suite { #[cfg(test)] mod tests { use ruff_formatter::format; - use ruff_python_parser::parse_suite; use crate::comments::Comments; diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap index 80c7ba60ec..0b680fd94c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__docstring_no_string_normalization.py.snap @@ -135,7 +135,7 @@ def multiline_backslash_3(): ```diff --- Black +++ Ruff -@@ -1,73 +1,75 @@ +@@ -1,24 +1,24 @@ class ALonelyClass: - ''' + """ @@ -167,96 +167,7 @@ def multiline_backslash_3(): pass - def foo(): -- """This is a docstring with -- some lines of text here -- """ -+ """This is a docstring with -+ some lines of text here -+ """ - return - - - def baz(): - '''"This" is a string with some -- embedded "quotes"''' -+ embedded "quotes"''' - return - - - def poit(): - """ -- Lorem ipsum dolor sit amet. -+ Lorem ipsum dolor sit amet. - -- Consectetur adipiscing elit: -- - sed do eiusmod tempor incididunt ut labore -- - dolore magna aliqua -- - enim ad minim veniam -- - quis nostrud exercitation ullamco laboris nisi -- - aliquip ex ea commodo consequat -- """ -+ Consectetur adipiscing elit: -+ - sed do eiusmod tempor incididunt ut labore -+ - dolore magna aliqua -+ - enim ad minim veniam -+ - quis nostrud exercitation ullamco laboris nisi -+ - aliquip ex ea commodo consequat -+ """ - pass - - - def under_indent(): - """ -- These lines are indented in a way that does not -- make sense. -- """ -+ These lines are indented in a way that does not -+make sense. -+ """ - pass - - - def over_indent(): - """ -- This has a shallow indent -- - But some lines are deeper -- - And the closing quote is too deep -+ This has a shallow indent -+ - But some lines are deeper -+ - And the closing quote is too deep - """ - pass - - - def single_line(): -- """But with a newline after it!""" -+ """But with a newline after it! -+ -+ """ - pass - - -@@ -83,41 +85,41 @@ - - def and_that(): - """ -- "hey yah" """ -+ "hey yah" """ - - - def and_this(): -- ''' -- "hey yah"''' -+ ''' -+ "hey yah"''' - - - def believe_it_or_not_this_is_in_the_py_stdlib(): -- ''' -- "hey yah"''' -+ ''' -+"hey yah"''' +@@ -97,27 +97,27 @@ def shockingly_the_quotes_are_normalized_v2(): @@ -285,14 +196,14 @@ def multiline_backslash_3(): - ''' - hey there \ ''' + """ -+ hey there \ """ ++ hey there \ """ def multiline_backslash_3(): - ''' - already escaped \\''' + """ -+ already escaped \\ """ ++ already escaped \\""" ``` ## Ruff Output @@ -323,53 +234,51 @@ def shockingly_the_quotes_are_normalized(): def foo(): - """This is a docstring with - some lines of text here - """ + """This is a docstring with + some lines of text here + """ return def baz(): '''"This" is a string with some - embedded "quotes"''' + embedded "quotes"''' return def poit(): """ - Lorem ipsum dolor sit amet. + Lorem ipsum dolor sit amet. - Consectetur adipiscing elit: - - sed do eiusmod tempor incididunt ut labore - - dolore magna aliqua - - enim ad minim veniam - - quis nostrud exercitation ullamco laboris nisi - - aliquip ex ea commodo consequat - """ + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ pass def under_indent(): """ - These lines are indented in a way that does not -make sense. - """ + These lines are indented in a way that does not + make sense. + """ pass def over_indent(): """ - This has a shallow indent - - But some lines are deeper - - And the closing quote is too deep + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep """ pass def single_line(): - """But with a newline after it! - - """ + """But with a newline after it!""" pass @@ -385,17 +294,17 @@ def that(): def and_that(): """ - "hey yah" """ + "hey yah" """ def and_this(): - ''' - "hey yah"''' + ''' + "hey yah"''' def believe_it_or_not_this_is_in_the_py_stdlib(): - ''' -"hey yah"''' + ''' + "hey yah"''' def shockingly_the_quotes_are_normalized_v2(): @@ -417,12 +326,12 @@ def multiline_backslash_1(): def multiline_backslash_2(): """ - hey there \ """ + hey there \ """ def multiline_backslash_3(): """ - already escaped \\ """ + already escaped \\""" ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap deleted file mode 100644 index e8c73055d0..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap +++ /dev/null @@ -1,105 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments_non_breaking_space.py ---- -## Input - -```py -from .config import ( ConfigTypeAttributes, Int, Path, # String, - # DEFAULT_TYPE_ATTRIBUTES, -) - -result = 1 # A simple comment -result = ( 1, ) # Another one - -result = 1 # type: ignore -result = 1# This comment is talking about type: ignore -square = Square(4) # type: Optional[Square] - -def function(a:int=42): - """ This docstring is already formatted - a - b - """ - #  There's a NBSP + 3 spaces before - # And 4 spaces on the next line - pass -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -14,9 +14,9 @@ - - - def function(a: int = 42): -- """This docstring is already formatted -- a -- b -+ """ This docstring is already formatted -+ a -+ b - """ - # There's a NBSP + 3 spaces before - # And 4 spaces on the next line -``` - -## Ruff Output - -```py -from .config import ( - ConfigTypeAttributes, - Int, - Path, # String, - # DEFAULT_TYPE_ATTRIBUTES, -) - -result = 1 # A simple comment -result = (1,) # Another one - -result = 1 #  type: ignore -result = 1 # This comment is talking about type: ignore -square = Square(4) #  type: Optional[Square] - - -def function(a: int = 42): - """ This docstring is already formatted - a - b - """ - # There's a NBSP + 3 spaces before - # And 4 spaces on the next line - pass -``` - -## Black Output - -```py -from .config import ( - ConfigTypeAttributes, - Int, - Path, # String, - # DEFAULT_TYPE_ATTRIBUTES, -) - -result = 1 # A simple comment -result = (1,) # Another one - -result = 1 #  type: ignore -result = 1 # This comment is talking about type: ignore -square = Square(4) #  type: Optional[Square] - - -def function(a: int = 42): - """This docstring is already formatted - a - b - """ - # There's a NBSP + 3 spaces before - # And 4 spaces on the next line - pass -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap deleted file mode 100644 index 60a50df3a5..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring.py.snap +++ /dev/null @@ -1,924 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/docstring.py ---- -## Input - -```py -class MyClass: - """ Multiline - class docstring - """ - - def method(self): - """Multiline - method docstring - """ - pass - - -def foo(): - """This is a docstring with - some lines of text here - """ - return - - -def bar(): - '''This is another docstring - with more lines of text - ''' - return - - -def baz(): - '''"This" is a string with some - embedded "quotes"''' - return - - -def troz(): - '''Indentation with tabs - is just as OK - ''' - return - - -def zort(): - """Another - multiline - docstring - """ - pass - -def poit(): - """ - Lorem ipsum dolor sit amet. - - Consectetur adipiscing elit: - - sed do eiusmod tempor incididunt ut labore - - dolore magna aliqua - - enim ad minim veniam - - quis nostrud exercitation ullamco laboris nisi - - aliquip ex ea commodo consequat - """ - pass - - -def under_indent(): - """ - These lines are indented in a way that does not -make sense. - """ - pass - - -def over_indent(): - """ - This has a shallow indent - - But some lines are deeper - - And the closing quote is too deep - """ - pass - - -def single_line(): - """But with a newline after it! - - """ - pass - - -def this(): - r""" - 'hey ho' - """ - - -def that(): - """ "hey yah" """ - - -def and_that(): - """ - "hey yah" """ - - -def and_this(): - ''' - "hey yah"''' - - -def multiline_whitespace(): - ''' - - - - - ''' - - -def oneline_whitespace(): - ''' ''' - - -def empty(): - """""" - - -def single_quotes(): - 'testing' - - -def believe_it_or_not_this_is_in_the_py_stdlib(): ''' -"hey yah"''' - - -def ignored_docstring(): - """a => \ -b""" - -def single_line_docstring_with_whitespace(): - """ This should be stripped """ - -def docstring_with_inline_tabs_and_space_indentation(): - """hey - - tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline - mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. - - line ends with some tabs - """ - - -def docstring_with_inline_tabs_and_tab_indentation(): - """hey - - tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline - mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. - - line ends with some tabs - """ - pass - - -def backslash_space(): - """\ """ - - -def multiline_backslash_1(): - ''' - hey\there\ - \ ''' - - -def multiline_backslash_2(): - ''' - hey there \ ''' - -# Regression test for #3425 -def multiline_backslash_really_long_dont_crash(): - """ - hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ - - -def multiline_backslash_3(): - ''' - already escaped \\ ''' - - -def my_god_its_full_of_stars_1(): - "I'm sorry Dave\u2001" - - -# the space below is actually a \u2001, removed in output -def my_god_its_full_of_stars_2(): - "I'm sorry Dave " - - -def docstring_almost_at_line_limit(): - """long docstring.................................................................""" - - -def docstring_almost_at_line_limit2(): - """long docstring................................................................. - - .................................................................................. - """ - - -def docstring_at_line_limit(): - """long docstring................................................................""" - - -def multiline_docstring_at_line_limit(): - """first line----------------------------------------------------------------------- - - second line----------------------------------------------------------------------""" - - -def stable_quote_normalization_with_immediate_inner_single_quote(self): - '''' - - - ''' -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,83 +1,85 @@ - class MyClass: -- """Multiline -- class docstring -- """ -+ """ Multiline -+ class docstring -+ """ - - def method(self): - """Multiline -- method docstring -- """ -+ method docstring -+ """ - pass - - - def foo(): -- """This is a docstring with -- some lines of text here -- """ -+ """This is a docstring with -+ some lines of text here -+ """ - return - - - def bar(): - """This is another docstring -- with more lines of text -- """ -+ with more lines of text -+ """ - return - - - def baz(): - '''"This" is a string with some -- embedded "quotes"''' -+ embedded "quotes"''' - return - - - def troz(): - """Indentation with tabs -- is just as OK -- """ -+ is just as OK -+ """ - return - - - def zort(): - """Another -- multiline -- docstring -- """ -+ multiline -+ docstring -+ """ - pass - - - def poit(): - """ -- Lorem ipsum dolor sit amet. -+ Lorem ipsum dolor sit amet. - -- Consectetur adipiscing elit: -- - sed do eiusmod tempor incididunt ut labore -- - dolore magna aliqua -- - enim ad minim veniam -- - quis nostrud exercitation ullamco laboris nisi -- - aliquip ex ea commodo consequat -- """ -+ Consectetur adipiscing elit: -+ - sed do eiusmod tempor incididunt ut labore -+ - dolore magna aliqua -+ - enim ad minim veniam -+ - quis nostrud exercitation ullamco laboris nisi -+ - aliquip ex ea commodo consequat -+ """ - pass - - - def under_indent(): - """ -- These lines are indented in a way that does not -- make sense. -- """ -+ These lines are indented in a way that does not -+make sense. -+ """ - pass - - - def over_indent(): - """ -- This has a shallow indent -- - But some lines are deeper -- - And the closing quote is too deep -+ This has a shallow indent -+ - But some lines are deeper -+ - And the closing quote is too deep - """ - pass - - - def single_line(): -- """But with a newline after it!""" -+ """But with a newline after it! -+ -+ """ - pass - - -@@ -93,20 +95,25 @@ - - def and_that(): - """ -- "hey yah" """ -+ "hey yah" """ - - - def and_this(): -- ''' -- "hey yah"''' -+ ''' -+ "hey yah"''' - - - def multiline_whitespace(): -- """ """ -+ """ -+ -+ -+ -+ -+ """ - - - def oneline_whitespace(): -- """ """ -+ """ """ - - - def empty(): -@@ -118,8 +125,8 @@ - - - def believe_it_or_not_this_is_in_the_py_stdlib(): -- ''' -- "hey yah"''' -+ ''' -+"hey yah"''' - - - def ignored_docstring(): -@@ -128,31 +135,31 @@ - - - def single_line_docstring_with_whitespace(): -- """This should be stripped""" -+ """ This should be stripped """ - - - def docstring_with_inline_tabs_and_space_indentation(): - """hey - - tab separated value -- tab at start of line and then a tab separated value -- multiple tabs at the beginning and inline -- mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. -- -- line ends with some tabs -+ tab at start of line and then a tab separated value -+ multiple tabs at the beginning and inline -+ mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. -+ -+ line ends with some tabs - """ - - - def docstring_with_inline_tabs_and_tab_indentation(): - """hey - -- tab separated value -- tab at start of line and then a tab separated value -- multiple tabs at the beginning and inline -- mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. -- -- line ends with some tabs -- """ -+ tab separated value -+ tab at start of line and then a tab separated value -+ multiple tabs at the beginning and inline -+ mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. -+ -+ line ends with some tabs -+ """ - pass - - -@@ -168,7 +175,7 @@ - - def multiline_backslash_2(): - """ -- hey there \ """ -+ hey there \ """ - - - # Regression test for #3425 -@@ -179,7 +186,7 @@ - - def multiline_backslash_3(): - """ -- already escaped \\""" -+ already escaped \\ """ - - - def my_god_its_full_of_stars_1(): -@@ -188,7 +195,7 @@ - - # the space below is actually a \u2001, removed in output - def my_god_its_full_of_stars_2(): -- "I'm sorry Dave" -+ "I'm sorry Dave " - - - def docstring_almost_at_line_limit(): -``` - -## Ruff Output - -```py -class MyClass: - """ Multiline - class docstring - """ - - def method(self): - """Multiline - method docstring - """ - pass - - -def foo(): - """This is a docstring with - some lines of text here - """ - return - - -def bar(): - """This is another docstring - with more lines of text - """ - return - - -def baz(): - '''"This" is a string with some - embedded "quotes"''' - return - - -def troz(): - """Indentation with tabs - is just as OK - """ - return - - -def zort(): - """Another - multiline - docstring - """ - pass - - -def poit(): - """ - Lorem ipsum dolor sit amet. - - Consectetur adipiscing elit: - - sed do eiusmod tempor incididunt ut labore - - dolore magna aliqua - - enim ad minim veniam - - quis nostrud exercitation ullamco laboris nisi - - aliquip ex ea commodo consequat - """ - pass - - -def under_indent(): - """ - These lines are indented in a way that does not -make sense. - """ - pass - - -def over_indent(): - """ - This has a shallow indent - - But some lines are deeper - - And the closing quote is too deep - """ - pass - - -def single_line(): - """But with a newline after it! - - """ - pass - - -def this(): - r""" - 'hey ho' - """ - - -def that(): - """ "hey yah" """ - - -def and_that(): - """ - "hey yah" """ - - -def and_this(): - ''' - "hey yah"''' - - -def multiline_whitespace(): - """ - - - - - """ - - -def oneline_whitespace(): - """ """ - - -def empty(): - """""" - - -def single_quotes(): - "testing" - - -def believe_it_or_not_this_is_in_the_py_stdlib(): - ''' -"hey yah"''' - - -def ignored_docstring(): - """a => \ -b""" - - -def single_line_docstring_with_whitespace(): - """ This should be stripped """ - - -def docstring_with_inline_tabs_and_space_indentation(): - """hey - - tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline - mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. - - line ends with some tabs - """ - - -def docstring_with_inline_tabs_and_tab_indentation(): - """hey - - tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline - mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. - - line ends with some tabs - """ - pass - - -def backslash_space(): - """\ """ - - -def multiline_backslash_1(): - """ - hey\there\ - \ """ - - -def multiline_backslash_2(): - """ - hey there \ """ - - -# Regression test for #3425 -def multiline_backslash_really_long_dont_crash(): - """ - hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ - - -def multiline_backslash_3(): - """ - already escaped \\ """ - - -def my_god_its_full_of_stars_1(): - "I'm sorry Dave\u2001" - - -# the space below is actually a \u2001, removed in output -def my_god_its_full_of_stars_2(): - "I'm sorry Dave " - - -def docstring_almost_at_line_limit(): - """long docstring.................................................................""" - - -def docstring_almost_at_line_limit2(): - """long docstring................................................................. - - .................................................................................. - """ - - -def docstring_at_line_limit(): - """long docstring................................................................""" - - -def multiline_docstring_at_line_limit(): - """first line----------------------------------------------------------------------- - - second line----------------------------------------------------------------------""" - - -def stable_quote_normalization_with_immediate_inner_single_quote(self): - """' - - - """ -``` - -## Black Output - -```py -class MyClass: - """Multiline - class docstring - """ - - def method(self): - """Multiline - method docstring - """ - pass - - -def foo(): - """This is a docstring with - some lines of text here - """ - return - - -def bar(): - """This is another docstring - with more lines of text - """ - return - - -def baz(): - '''"This" is a string with some - embedded "quotes"''' - return - - -def troz(): - """Indentation with tabs - is just as OK - """ - return - - -def zort(): - """Another - multiline - docstring - """ - pass - - -def poit(): - """ - Lorem ipsum dolor sit amet. - - Consectetur adipiscing elit: - - sed do eiusmod tempor incididunt ut labore - - dolore magna aliqua - - enim ad minim veniam - - quis nostrud exercitation ullamco laboris nisi - - aliquip ex ea commodo consequat - """ - pass - - -def under_indent(): - """ - These lines are indented in a way that does not - make sense. - """ - pass - - -def over_indent(): - """ - This has a shallow indent - - But some lines are deeper - - And the closing quote is too deep - """ - pass - - -def single_line(): - """But with a newline after it!""" - pass - - -def this(): - r""" - 'hey ho' - """ - - -def that(): - """ "hey yah" """ - - -def and_that(): - """ - "hey yah" """ - - -def and_this(): - ''' - "hey yah"''' - - -def multiline_whitespace(): - """ """ - - -def oneline_whitespace(): - """ """ - - -def empty(): - """""" - - -def single_quotes(): - "testing" - - -def believe_it_or_not_this_is_in_the_py_stdlib(): - ''' - "hey yah"''' - - -def ignored_docstring(): - """a => \ -b""" - - -def single_line_docstring_with_whitespace(): - """This should be stripped""" - - -def docstring_with_inline_tabs_and_space_indentation(): - """hey - - tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline - mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. - - line ends with some tabs - """ - - -def docstring_with_inline_tabs_and_tab_indentation(): - """hey - - tab separated value - tab at start of line and then a tab separated value - multiple tabs at the beginning and inline - mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. - - line ends with some tabs - """ - pass - - -def backslash_space(): - """\ """ - - -def multiline_backslash_1(): - """ - hey\there\ - \ """ - - -def multiline_backslash_2(): - """ - hey there \ """ - - -# Regression test for #3425 -def multiline_backslash_really_long_dont_crash(): - """ - hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ - - -def multiline_backslash_3(): - """ - already escaped \\""" - - -def my_god_its_full_of_stars_1(): - "I'm sorry Dave\u2001" - - -# the space below is actually a \u2001, removed in output -def my_god_its_full_of_stars_2(): - "I'm sorry Dave" - - -def docstring_almost_at_line_limit(): - """long docstring.................................................................""" - - -def docstring_almost_at_line_limit2(): - """long docstring................................................................. - - .................................................................................. - """ - - -def docstring_at_line_limit(): - """long docstring................................................................""" - - -def multiline_docstring_at_line_limit(): - """first line----------------------------------------------------------------------- - - second line----------------------------------------------------------------------""" - - -def stable_quote_normalization_with_immediate_inner_single_quote(self): - """' - - - """ -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap index 427408a63c..dad30c4cb8 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__docstring_preview.py.snap @@ -62,11 +62,7 @@ def single_quote_docstring_over_line_limit2(): ```diff --- Black +++ Ruff -@@ -1,9 +1,11 @@ - def docstring_almost_at_line_limit(): -- """long docstring.................................................................""" -+ """long docstring................................................................. -+ """ +@@ -3,7 +3,8 @@ def docstring_almost_at_line_limit_with_prefix(): @@ -82,8 +78,7 @@ def single_quote_docstring_over_line_limit2(): ```py def docstring_almost_at_line_limit(): - """long docstring................................................................. - """ + """long docstring.................................................................""" def docstring_almost_at_line_limit_with_prefix(): diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap new file mode 100644 index 0000000000..0a3c9a62a6 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap @@ -0,0 +1,448 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring.py +--- +## Input +```py +def single_line_backslashes1(): + """ content\ """ + return + + +def single_line_backslashes2(): + """ content\\ """ + return + + +def single_line_backslashes3(): + """ content\\\ """ + return + + +def multiline_backslashes1(): + """This is a docstring with + some lines of text\ """ + return + + +def multiline_backslashes2(): + """This is a docstring with + some lines of text\\ """ + return + + +def multiline_backslashes3(): + """This is a docstring with + some lines of text\\\ """ + return + + +def multiple_negatively_indented_docstring_lines(): + """a + b + c + d + e + """ + + +def overindentend_docstring(): + """a + over-indented + """ + + +def comment_before_docstring(): + # don't lose this function comment ... + """Does nothing. + + But it has comments + """ # ... neither lose this function comment + + +class CommentBeforeDocstring(): + # don't lose this class comment ... + """Empty class. + + But it has comments + """ # ... neither lose this class comment + + +class IndentMeSome: + def doc_string_without_linebreak_after_colon(self): """ This is somewhat strange + a + b + We format this a is the docstring had started properly indented on the next + line if the target indentation. This may we incorrect since source and target + indentation can be incorrect, but this is also an edge case. + """ + + +class IgnoreImplicitlyConcatenatedStrings: + """""" "" + + +def docstring_that_ends_with_quote_and_a_line_break1(): + """ + he said "the news of my death have been greatly exaggerated" + """ + + +def docstring_that_ends_with_quote_and_a_line_break2(): + """he said "the news of my death have been greatly exaggerated" + """ + + +def docstring_that_ends_with_quote_and_a_line_break3(): + """he said "the news of my death have been greatly exaggerated" + + """ + + +class TabbedIndent: + def tabbed_indent(self): + """check for correct tabbed formatting + ^^^^^^^^^^ + Normal indented line + - autor + """ +``` + +## Outputs +### Output 1 +``` +indent-style = Spaces, size: 4 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def single_line_backslashes1(): + """content\ """ + return + + +def single_line_backslashes2(): + """content\\""" + return + + +def single_line_backslashes3(): + """content\\\ """ + return + + +def multiline_backslashes1(): + """This is a docstring with + some lines of text\ """ + return + + +def multiline_backslashes2(): + """This is a docstring with + some lines of text\\""" + return + + +def multiline_backslashes3(): + """This is a docstring with + some lines of text\\\ """ + return + + +def multiple_negatively_indented_docstring_lines(): + """a + b + c + d + e + """ + + +def overindentend_docstring(): + """a + over-indented + """ + + +def comment_before_docstring(): + # don't lose this function comment ... + """Does nothing. + + But it has comments + """ # ... neither lose this function comment + + +class CommentBeforeDocstring: + # don't lose this class comment ... + """Empty class. + + But it has comments + """ # ... neither lose this class comment + + +class IndentMeSome: + def doc_string_without_linebreak_after_colon(self): + """This is somewhat strange + a + b + We format this a is the docstring had started properly indented on the next + line if the target indentation. This may we incorrect since source and target + indentation can be incorrect, but this is also an edge case. + """ + + +class IgnoreImplicitlyConcatenatedStrings: + """""" "" + + +def docstring_that_ends_with_quote_and_a_line_break1(): + """ + he said "the news of my death have been greatly exaggerated" + """ + + +def docstring_that_ends_with_quote_and_a_line_break2(): + """he said "the news of my death have been greatly exaggerated" """ + + +def docstring_that_ends_with_quote_and_a_line_break3(): + """he said "the news of my death have been greatly exaggerated" """ + + +class TabbedIndent: + def tabbed_indent(self): + """check for correct tabbed formatting + ^^^^^^^^^^ + Normal indented line + - autor + """ +``` + + +### Output 2 +``` +indent-style = Spaces, size: 2 +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def single_line_backslashes1(): + """content\ """ + return + + +def single_line_backslashes2(): + """content\\""" + return + + +def single_line_backslashes3(): + """content\\\ """ + return + + +def multiline_backslashes1(): + """This is a docstring with + some lines of text\ """ + return + + +def multiline_backslashes2(): + """This is a docstring with + some lines of text\\""" + return + + +def multiline_backslashes3(): + """This is a docstring with + some lines of text\\\ """ + return + + +def multiple_negatively_indented_docstring_lines(): + """a + b + c + d + e + """ + + +def overindentend_docstring(): + """a + over-indented + """ + + +def comment_before_docstring(): + # don't lose this function comment ... + """Does nothing. + + But it has comments + """ # ... neither lose this function comment + + +class CommentBeforeDocstring: + # don't lose this class comment ... + """Empty class. + + But it has comments + """ # ... neither lose this class comment + + +class IndentMeSome: + def doc_string_without_linebreak_after_colon(self): + """This is somewhat strange + a + b + We format this a is the docstring had started properly indented on the next + line if the target indentation. This may we incorrect since source and target + indentation can be incorrect, but this is also an edge case. + """ + + +class IgnoreImplicitlyConcatenatedStrings: + """""" "" + + +def docstring_that_ends_with_quote_and_a_line_break1(): + """ + he said "the news of my death have been greatly exaggerated" + """ + + +def docstring_that_ends_with_quote_and_a_line_break2(): + """he said "the news of my death have been greatly exaggerated" """ + + +def docstring_that_ends_with_quote_and_a_line_break3(): + """he said "the news of my death have been greatly exaggerated" """ + + +class TabbedIndent: + def tabbed_indent(self): + """check for correct tabbed formatting + ^^^^^^^^^^ + Normal indented line + - autor + """ +``` + + +### Output 3 +``` +indent-style = Tab +line-width = 88 +quote-style = Double +magic-trailing-comma = Respect +``` + +```py +def single_line_backslashes1(): + """content\ """ + return + + +def single_line_backslashes2(): + """content\\""" + return + + +def single_line_backslashes3(): + """content\\\ """ + return + + +def multiline_backslashes1(): + """This is a docstring with + some lines of text\ """ + return + + +def multiline_backslashes2(): + """This is a docstring with + some lines of text\\""" + return + + +def multiline_backslashes3(): + """This is a docstring with + some lines of text\\\ """ + return + + +def multiple_negatively_indented_docstring_lines(): + """a + b + c + d + e + """ + + +def overindentend_docstring(): + """a + over-indented + """ + + +def comment_before_docstring(): + # don't lose this function comment ... + """Does nothing. + + But it has comments + """ # ... neither lose this function comment + + +class CommentBeforeDocstring: + # don't lose this class comment ... + """Empty class. + + But it has comments + """ # ... neither lose this class comment + + +class IndentMeSome: + def doc_string_without_linebreak_after_colon(self): + """This is somewhat strange + a + b + We format this a is the docstring had started properly indented on the next + line if the target indentation. This may we incorrect since source and target + indentation can be incorrect, but this is also an edge case. + """ + + +class IgnoreImplicitlyConcatenatedStrings: + """""" "" + + +def docstring_that_ends_with_quote_and_a_line_break1(): + """ + he said "the news of my death have been greatly exaggerated" + """ + + +def docstring_that_ends_with_quote_and_a_line_break2(): + """he said "the news of my death have been greatly exaggerated" """ + + +def docstring_that_ends_with_quote_and_a_line_break3(): + """he said "the news of my death have been greatly exaggerated" """ + + +class TabbedIndent: + def tabbed_indent(self): + """check for correct tabbed formatting + ^^^^^^^^^^ + Normal indented line + - autor + """ +``` + + +