diff --git a/.gitattributes b/.gitattributes index 23c253a26d08a..8dd4fe466ad6e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,5 +3,8 @@ crates/ruff_linter/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf +crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py text eol=crlf +crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap text eol=crlf + ruff.schema.json linguist-generated=true text=auto eol=lf *.md.snap linguist-language=Markdown diff --git a/Cargo.lock b/Cargo.lock index b29c2bd77ae0c..ee526538f941e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2334,6 +2334,7 @@ dependencies = [ "itertools 0.11.0", "memchr", "once_cell", + "regex", "ruff_cache", "ruff_formatter", "ruff_macros", diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index 24325082f0f99..5bc38d4960f15 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -41,6 +41,7 @@ unicode-width = { workspace = true } ruff_formatter = { path = "../ruff_formatter" } insta = { workspace = true, features = ["glob"] } +regex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } similar = { workspace = true } diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.options.json new file mode 100644 index 0000000000000..b2a2d8a4e537d --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.options.json @@ -0,0 +1,42 @@ +[ + { + "docstring_code": "disabled", + "indent_style": "space", + "indent_width": 4 + }, + { + "docstring_code": "disabled", + "indent_style": "space", + "indent_width": 2 + }, + { + "docstring_code": "disabled", + "indent_style": "tab", + "indent_width": 8 + }, + { + "docstring_code": "disabled", + "indent_style": "tab", + "indent_width": 4 + }, + { + "docstring_code": "enabled", + "indent_style": "space", + "indent_width": 4 + }, + { + "docstring_code": "enabled", + "indent_style": "space", + "indent_width": 2 + }, + { + "docstring_code": "enabled", + "indent_style": "tab", + "indent_width": 8 + }, + { + "docstring_code": "enabled", + "indent_style": "tab", + "indent_width": 4 + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.py new file mode 100644 index 0000000000000..1050c71a31847 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.py @@ -0,0 +1,316 @@ +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + >>> cool_stuff( y ) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... + ... print( x ) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested( x ): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + ''' + Do cool stuff. + + >>> assert ("Easy!") + >>> import math + >>> math.floor( 1.9 ) + 1 + ''' + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + ''' + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard) + ''' + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( x ) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.options.json new file mode 100644 index 0000000000000..6af4394568536 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.options.json @@ -0,0 +1,8 @@ +[ + { + "docstring_code": "enabled", + "indent_style": "space", + "indent_width": 4, + "line_ending": "CarriageReturnLineFeed" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py new file mode 100644 index 0000000000000..05cc97963f21e --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py @@ -0,0 +1,9 @@ +def doctest_line_ending(): + """ + Do cool stuff. + >>> def foo( x ): + ... print( x ) + ... + ... print( x ) + """ + pass diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index f92825fe958f4..eb8fe7edf4155 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -1,5 +1,5 @@ use crate::comments::Comments; -use crate::PyFormatOptions; +use crate::{PyFormatOptions, QuoteStyle}; use ruff_formatter::{Buffer, FormatContext, GroupId, SourceCode}; use ruff_source_file::Locator; use std::fmt::{Debug, Formatter}; @@ -11,6 +11,15 @@ pub struct PyFormatContext<'a> { contents: &'a str, comments: Comments<'a>, node_level: NodeLevel, + /// Set to a non-None value when the formatter is running on a code + /// snippet within a docstring. The value should be the quote style of the + /// docstring containing the code snippet. + /// + /// Various parts of the formatter may inspect this state to change how it + /// works. For example, multi-line strings will always be written with a + /// quote style that is inverted from the one here in order to ensure that + /// the formatted Python code will be valid. + docstring: Option, } impl<'a> PyFormatContext<'a> { @@ -20,6 +29,7 @@ impl<'a> PyFormatContext<'a> { contents, comments, node_level: NodeLevel::TopLevel(TopLevelStatementPosition::Other), + docstring: None, } } @@ -43,6 +53,27 @@ impl<'a> PyFormatContext<'a> { pub(crate) fn comments(&self) -> &Comments<'a> { &self.comments } + + /// Returns a non-None value only if the formatter is running on a code + /// snippet within a docstring. + /// + /// The quote style returned corresponds to the quoting used for the + /// docstring containing the code snippet currently being formatted. + pub(crate) fn docstring(&self) -> Option { + self.docstring + } + + /// Return a new context suitable for formatting code snippets within a + /// docstring. + /// + /// The quote style given should correspond to the style of quoting used + /// for the docstring containing the code snippets. + pub(crate) fn in_docstring(self, style: QuoteStyle) -> PyFormatContext<'a> { + PyFormatContext { + docstring: Some(style), + ..self + } + } } impl FormatContext for PyFormatContext<'_> { diff --git a/crates/ruff_python_formatter/src/expression/string.rs b/crates/ruff_python_formatter/src/expression/string.rs index cb534d49a4517..2c711ac60ef1f 100644 --- a/crates/ruff_python_formatter/src/expression/string.rs +++ b/crates/ruff_python_formatter/src/expression/string.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use bitflags::bitflags; -use ruff_formatter::{format_args, write}; +use ruff_formatter::{format_args, write, Printed}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::{ self as ast, ExprBytesLiteral, ExprFString, ExprStringLiteral, ExpressionRef, @@ -16,9 +16,10 @@ use crate::expression::parentheses::{ }; use crate::expression::Expr; use crate::prelude::*; +use crate::FormatModuleError; use crate::QuoteStyle; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] enum Quoting { CanChange, Preserve, @@ -189,6 +190,7 @@ impl<'a> FormatString<'a> { impl<'a> Format> for FormatString<'a> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + let parent_docstring_quote_style = f.context().docstring(); let locator = f.context().locator(); let result = match self.layout { StringLayout::Default => { @@ -200,14 +202,19 @@ impl<'a> Format> for FormatString<'a> { self.string.quoting(&locator), &locator, f.options().quote_style(), + parent_docstring_quote_style, ) .fmt(f) } } StringLayout::DocString => { let string_part = StringPart::from_source(self.string.range(), &locator); - let normalized = - string_part.normalize(Quoting::CanChange, &locator, f.options().quote_style()); + let normalized = string_part.normalize( + Quoting::CanChange, + &locator, + f.options().quote_style(), + parent_docstring_quote_style, + ); format_docstring(&normalized, f) } StringLayout::ImplicitConcatenatedStringInBinaryLike => { @@ -243,6 +250,7 @@ impl Format> for FormatStringContinuation<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { let comments = f.context().comments().clone(); let locator = f.context().locator(); + let in_docstring = f.context().docstring(); let quote_style = f.options().quote_style(); let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space()); @@ -252,6 +260,7 @@ impl Format> for FormatStringContinuation<'_> { self.string.quoting(&locator), &locator, quote_style, + in_docstring, ); joiner.entry(&format_args![ @@ -301,16 +310,72 @@ impl StringPart { } /// Computes the strings preferred quotes and normalizes its content. + /// + /// The parent docstring quote style should be set when formatting a code + /// snippet within the docstring. The quote style should correspond to the + /// style of quotes used by said docstring. Normalization will ensure the + /// quoting styles don't conflict. fn normalize<'a>( self, quoting: Quoting, locator: &'a Locator, configured_style: QuoteStyle, + parent_docstring_quote_style: Option, ) -> NormalizedString<'a> { - // Per PEP 8 and PEP 257, always prefer double quotes for docstrings and triple-quoted - // strings. (We assume docstrings are always triple-quoted.) + // Per PEP 8 and PEP 257, always prefer double quotes for docstrings + // and triple-quoted strings. (We assume docstrings are always + // triple-quoted.) let preferred_style = if self.quotes.triple { - QuoteStyle::Double + // ... unless we're formatting a code snippet inside a docstring, + // then we specifically want to invert our quote style to avoid + // writing out invalid Python. + // + // It's worth pointing out that we can actually wind up being + // somewhat out of sync with PEP8 in this case. Consider this + // example: + // + // def foo(): + // ''' + // Something. + // + // >>> """tricksy""" + // ''' + // pass + // + // Ideally, this would be reformatted as: + // + // def foo(): + // """ + // Something. + // + // >>> '''tricksy''' + // """ + // pass + // + // But the logic here results in the original quoting being + // preserved. This is because the quoting style of the outer + // docstring is determined, in part, by looking at its contents. In + // this case, it notices that it contains a `"""` and thus infers + // that using `'''` would overall read better because it avoids + // the need to escape the interior `"""`. Except... in this case, + // the `"""` is actually part of a code snippet that could get + // reformatted to using a different quoting style itself. + // + // Fixing this would, I believe, require some fairly seismic + // changes to how formatting strings works. Namely, we would need + // to look for code snippets before normalizing the docstring, and + // then figure out the quoting style more holistically by looking + // at the various kinds of quotes used in the code snippets and + // what reformatting them might look like. + // + // Overall this is a bit of a corner case and just inverting the + // style from what the parent ultimately decided upon works, even + // if it doesn't have perfect alignment with PEP8. + if let Some(style) = parent_docstring_quote_style { + style.invert() + } else { + QuoteStyle::Double + } } else { configured_style }; @@ -924,7 +989,7 @@ fn format_docstring(normalized: &NormalizedString, f: &mut PyFormatter) -> Forma // 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 + let stripped_indentation_length = lines .clone() // We don't want to count whitespace-only lines as miss-indented .filter(|line| !line.trim().is_empty()) @@ -932,19 +997,15 @@ fn format_docstring(normalized: &NormalizedString, f: &mut PyFormatter) -> Forma .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(); + DocstringLinePrinter { + f, + offset, + stripped_indentation_length, + already_normalized, + quote_style: normalized.quotes.style, + code_example: CodeExample::default(), } + .add_iter(lines)?; // Same special case in the last line as for the first line let trim_end = docstring @@ -957,74 +1018,537 @@ fn format_docstring(normalized: &NormalizedString, f: &mut PyFormatter) -> Forma write!(f, [source_position(normalized.end()), normalized.quotes]) } -/// 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(normalized: &NormalizedString, trim_end: &str) -> bool { - trim_end.ends_with(normalized.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, +/// An abstraction for printing each line of a docstring. +struct DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { + f: &'fmt mut PyFormatter<'ast, 'buf>, + /// The source offset of the beginning of the line that is currently being + /// printed. offset: TextSize, + /// Indentation alignment based on the least indented line in the + /// docstring. stripped_indentation_length: TextSize, + /// Whether the docstring is overall already considered normalized. When it + /// is, the formatter can take a fast path. 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(()) + /// The quote style used by the docstring being printed. + quote_style: QuoteStyle, + /// The current code example detected in the docstring. + code_example: CodeExample<'src>, +} + +impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { + /// Print all of the lines in the given iterator to this + /// printer's formatter. + /// + /// Note that callers may treat the first line specially, such that the + /// iterator given contains all lines except for the first. + fn add_iter( + &mut self, + mut lines: std::iter::Peekable>, + ) -> FormatResult<()> { + while let Some(line) = lines.next() { + let line = DocstringLine { + line: Cow::Borrowed(line), + offset: self.offset, + is_last: lines.peek().is_none(), + }; + // We know that the normalized string has \n line endings. + self.offset += line.line.text_len() + "\n".text_len(); + self.add_one(line)?; + } + Ok(()) + } + + /// Adds the given line to this printer. + /// + /// Depending on what's in the line, this may or may not print the line + /// immediately to the underlying buffer. If the line starts or is part + /// of an existing code snippet, then the lines will get buffered until + /// the code snippet is complete. + fn add_one(&mut self, line: DocstringLine<'src>) -> FormatResult<()> { + // Just pass through the line as-is without looking for a code snippet + // when docstring code formatting is disabled. And also when we are + // formatting a code snippet so as to avoid arbitrarily nested code + // snippet formatting. We avoid this because it's likely quite tricky + // to get right 100% of the time, although perhaps not impossible. It's + // not clear that it's worth the effort to support. + if !self.f.options().docstring_code().is_enabled() || self.f.context().docstring().is_some() + { + return self.print_one(&line); + } + match self.code_example.add(line) { + CodeExampleAddAction::Print { original } => self.print_one(&original)?, + CodeExampleAddAction::Kept => {} + CodeExampleAddAction::Format { + kind, + code, + original, + } => { + let Some(formatted_lines) = self.format(&code)? else { + // If formatting failed in a way that should not be + // allowed, we back out what we're doing and print the + // original lines we found as-is as if we did nothing. + for codeline in code { + self.print_one(&codeline.original)?; + } + if let Some(original) = original { + self.print_one(&original)?; + } + return Ok(()); + }; + + self.already_normalized = false; + match kind { + CodeExampleKind::Doctest(CodeExampleDoctest { indent }) => { + let mut lines = formatted_lines.into_iter(); + if let Some(first) = lines.next() { + self.print_one(&first.map(|line| std::format!("{indent}>>> {line}")))?; + for docline in lines { + self.print_one( + &docline.map(|line| std::format!("{indent}... {line}")), + )?; + } + } + } + } + if let Some(original) = original { + self.print_one(&original)?; + } + } + } + Ok(()) + } + + /// Prints the single line given. + /// + /// This mostly just handles indentation and ensuring line breaks are + /// inserted as appropriate before passing it on to the formatter to + /// print to the buffer. + fn print_one(&mut self, line: &DocstringLine<'_>) -> FormatResult<()> { + let trim_end = line.line.trim_end(); + if trim_end.is_empty() { + return if line.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(self.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 case 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 = indentation_length(trim_end) - self.stripped_indentation_length; + let in_docstring_indent = " ".repeat(usize::from(indent_len)) + trim_end.trim_start(); + text(&in_docstring_indent, Some(line.offset)).fmt(self.f)?; } else { - empty_line().fmt(f) + // Take the string with the trailing whitespace removed, then also + // skip the leading whitespace. + let trimmed_line_range = TextRange::at(line.offset, trim_end.text_len()) + .add_start(self.stripped_indentation_length); + if self.already_normalized { + source_text_slice(trimmed_line_range).fmt(self.f)?; + } else { + // All indents are ascii spaces, so the slicing is correct. + text( + &trim_end[usize::from(self.stripped_indentation_length)..], + Some(trimmed_line_range.start()), + ) + .fmt(self.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 !line.is_last { + hard_line_break().fmt(self.f)?; + } + + Ok(()) + } + + /// Given a sequence of lines from a code snippet, format them and return + /// the formatted code as a sequence of owned docstring lines. + /// + /// 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 + /// resulting reformatted code snippet is invalid Python), then `Ok(None)` + /// is returned. In this case, callers should assume that a reformatted + /// code snippet is unavailable and bail out of trying to format it. + /// + /// Currently, when the above cases happen and `Ok(None)` is returned, the + /// routine is silent about it. So from the user's perspective, this will + /// fail silently. Ideally, this would at least emit a warning message, + /// but at time of writing, it wasn't clear to me how to best do that. + fn format( + &mut self, + code: &[CodeExampleLine<'src>], + ) -> FormatResult>>> { + use ruff_python_parser::AsMode; + + let offset = code + .get(0) + .expect("code blob must be non-empty") + .original + .offset; + let last_line_is_last = code + .last() + .expect("code blob must be non-empty") + .original + .is_last; + let codeblob = code + .iter() + .map(|line| &*line.code) + .collect::>() + .join("\n"); + let printed = match docstring_format_source(self.f.options(), self.quote_style, &codeblob) { + Ok(printed) => printed, + Err(FormatModuleError::FormatError(err)) => return Err(err), + Err( + FormatModuleError::LexError(_) + | FormatModuleError::ParseError(_) + | FormatModuleError::PrintError(_), + ) => { + return Ok(None); + } + }; + // This is a little hokey, but we want to determine whether the + // reformatted code snippet will lead to an overall invalid docstring. + // So attempt to parse it as Python code, but ensure it is wrapped + // within a docstring using the same quotes as the docstring we're in + // right now. + // + // This is an unfortunate stop-gap to attempt to prevent us from + // writing invalid Python due to some oddity of the code snippet within + // a docstring. As we fix corner cases over time, we can perhaps + // remove this check. See the `doctest_invalid_skipped` tests in + // `docstring_code_examples.py` for when this check is relevant. + let wrapped = match self.quote_style { + QuoteStyle::Single => std::format!("'''{}'''", printed.as_code()), + QuoteStyle::Double => std::format!(r#""""{}""""#, printed.as_code()), }; + let result = ruff_python_parser::parse( + &wrapped, + self.f.options().source_type().as_mode(), + "", + ); + // If the resulting code is not valid, then reset and pass through + // the docstring lines as-is. + if result.is_err() { + return Ok(None); + } + let mut lines = printed + .as_code() + .lines() + .map(|line| DocstringLine { + line: Cow::Owned(line.into()), + offset, + is_last: false, + }) + .collect::>(); + if let Some(last) = lines.last_mut() { + last.is_last = last_line_is_last; + } + Ok(Some(lines)) } +} - 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 = indentation_length(trim_end) - stripped_indentation_length; - let in_docstring_indent = " ".repeat(usize::from(indent_len)) + trim_end.trim_start(); - 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_length); - if already_normalized { - source_text_slice(trimmed_line_range).fmt(f)?; - } else { - // All indents are ascii spaces, so the slicing is correct - text( - &trim_end[usize::from(stripped_indentation_length)..], - Some(trimmed_line_range.start()), - ) - .fmt(f)?; +/// Represents a single line in a docstring. +/// +/// This type is used to both represent the original lines in a docstring +/// (the line will be borrowed) and also the newly formatted lines from code +/// snippets (the line will be owned). +#[derive(Clone, Debug)] +struct DocstringLine<'src> { + /// The actual text of the line, not including the line terminator. + line: Cow<'src, str>, + /// The offset into the source document which this line corresponds to. + offset: TextSize, + /// Whether this is the last line in a docstring or not. "Last" lines have + /// some special treatment when printing. + is_last: bool, +} + +impl<'src> DocstringLine<'src> { + /// Return this line, but with the given function applied to the text of + /// the line. + fn map(self, mut map: impl FnMut(&str) -> String) -> DocstringLine<'static> { + DocstringLine { + line: Cow::Owned(map(&self.line)), + ..self } } +} + +/// A single code example extracted from a docstring. +/// +/// This represents an intermediate state from when the code example was first +/// found all the way up until the point at which the code example has finished +/// and is reformatted. +/// +/// Its default state is "empty." That is, that no code example is currently +/// being collected. +#[derive(Debug, Default)] +struct CodeExample<'src> { + /// The kind of code example being collected, or `None` if no code example + /// has been observed. + kind: Option, + /// The lines that have been seen so far that make up the code example. + lines: Vec>, +} - // 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)?; +impl<'src> CodeExample<'src> { + /// Attempt to add an original line from a docstring to this code example. + /// + /// Based on the line and the internal state of whether a code example is + /// currently being collected or not, this will return an "action" for + /// the caller to perform. The typical case is a "print" action, which + /// instructs the caller to just print the line as though it were not part + /// of a code snippet. + fn add(&mut self, original: DocstringLine<'src>) -> CodeExampleAddAction<'src> { + match self.kind.take() { + // There's no existing code example being built, so we look for + // the start of one or otherwise tell the caller we couldn't find + // anything. + None => match self.add_start(original) { + None => CodeExampleAddAction::Kept, + Some(original) => CodeExampleAddAction::Print { original }, + }, + Some(CodeExampleKind::Doctest(doctest)) => { + if let Some(code) = doctest_find_ps2_prompt(&doctest.indent, &original.line) { + let code = code.to_string(); + self.lines.push(CodeExampleLine { original, code }); + // Stay with the doctest kind while we accumulate all + // PS2 prompts. + self.kind = Some(CodeExampleKind::Doctest(doctest)); + return CodeExampleAddAction::Kept; + } + let code = std::mem::take(&mut self.lines); + let original = self.add_start(original); + CodeExampleAddAction::Format { + code, + kind: CodeExampleKind::Doctest(doctest), + original, + } + } + } + } + + /// Looks for the start of a code example. If one was found, then the given + /// line is kept and added as part of the code example. Otherwise, the line + /// is returned unchanged and no code example was found. + /// + /// # Panics + /// + /// This panics when the existing code-example is any non-None value. That + /// is, this routine assumes that there is no ongoing code example being + /// collected and looks for the beginning of another code example. + fn add_start(&mut self, original: DocstringLine<'src>) -> Option> { + assert_eq!(None, self.kind, "expected no existing code example"); + if let Some((indent, code)) = doctest_find_ps1_prompt(&original.line) { + let indent = indent.to_string(); + let code = code.to_string(); + self.lines.push(CodeExampleLine { original, code }); + self.kind = Some(CodeExampleKind::Doctest(CodeExampleDoctest { indent })); + return None; + } + Some(original) } +} + +/// The kind of code example observed in a docstring. +#[derive(Clone, Debug, Eq, PartialEq)] +enum CodeExampleKind { + /// Code found in Python "doctests." + /// + /// Documentation describing doctests and how they're recognized can be + /// found as part of the Python standard library: + /// https://docs.python.org/3/library/doctest.html. + /// + /// (You'll likely need to read the regex matching used internally by the + /// doctest module to determine more precisely how it works.) + Doctest(CodeExampleDoctest), +} + +/// State corresponding to a single doctest code example found in a docstring. +#[derive(Clone, Debug, Eq, PartialEq)] +struct CodeExampleDoctest { + /// The indent observed in the first doctest line. + /// + /// More precisely, this corresponds to the whitespace observed before + /// the starting `>>> ` (the "PS1 prompt"). + indent: String, +} - Ok(()) +/// A single line in a code example found in a docstring. +/// +/// A code example line exists prior to formatting, and is thus in full +/// correspondence with the original lines from the docstring. Indeed, a +/// code example line includes both the original line *and* the actual code +/// extracted from the line. For example, if a line in a docstring is `>>> +/// foo(x)`, then the original line is `>>> foo(x)` and the code portion is +/// `foo(x)`. +/// +/// The original line is kept for things like offset information, but also +/// because it may still be needed if it turns out that the code snippet is +/// not valid or otherwise could not be formatted. In which case, the original +/// lines are printed as-is. +#[derive(Debug)] +struct CodeExampleLine<'src> { + /// The normalized (but original) line from the doc string. This might, for + /// example, contain a `>>> ` or `... ` prefix if this code example is a + /// doctest. + original: DocstringLine<'src>, + /// The code extracted from the line. + code: String, +} + +/// An action that a caller should perform after attempting to add a line from +/// a docstring to a code example. +/// +/// Callers are expected to add every line from a docstring to a code example, +/// and the state of the code example (and the line itself) will determine +/// how the caller should react. +#[derive(Debug)] +enum CodeExampleAddAction<'src> { + /// The line added was ignored by `CodeExample` and the caller should print + /// it to the formatter as-is. + /// + /// This is the common case. That is, most lines in most docstrings are not + /// part of a code example. + Print { original: DocstringLine<'src> }, + /// The line added was kept by `CodeExample` as part of a new or existing + /// code example. + /// + /// When this occurs, callers should not try to format the line and instead + /// move on to the next line. + Kept, + /// The line added indicated that the code example is finished and should + /// be formatted and printed. The line added is not treated as part of + /// the code example. If the line added indicated the start of another + /// code example, then is won't be returned to the caller here. Otherwise, + /// callers should pass it through to the formatter as-is. + Format { + /// The kind of code example that was found. + kind: CodeExampleKind, + /// The Python code that should be formatted, indented and printed. + /// + /// This is guaranteed to be non-empty. + code: Vec>, + /// When set, the line is considered not part of any code example + /// and should be formatted as if the `Ignore` action were returned. + /// Otherwise, if there is no line, then either one does not exist + /// or it is part of another code example and should be treated as a + /// `Kept` action. + original: Option>, + }, +} + +/// Looks for a valid doctest PS1 prompt in the line given. +/// +/// If one was found, then the indentation prior to the prompt is returned +/// along with the code portion of the line. +fn doctest_find_ps1_prompt(line: &str) -> Option<(&str, &str)> { + let trim_start = line.trim_start(); + // Prompts must be followed by an ASCII space character[1]. + // + // [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L809-L812 + let code = trim_start.strip_prefix(">>> ")?; + let indent_len = line + .len() + .checked_sub(trim_start.len()) + .expect("suffix is <= original"); + let indent = &line[..indent_len]; + Some((indent, code)) +} + +/// Looks for a valid doctest PS2 prompt in the line given. +/// +/// If one is found, then the code portion of the line following the PS2 prompt +/// is returned. +/// +/// Callers must provide a string containing the original indentation of the +/// PS1 prompt that started the doctest containing the potential PS2 prompt +/// in the line given. If the line contains a PS2 prompt, its indentation must +/// match the indentation used for the corresponding PS1 prompt (otherwise +/// `None` will be returned). +fn doctest_find_ps2_prompt<'src>(ps1_indent: &str, line: &'src str) -> Option<&'src str> { + let (ps2_indent, ps2_after) = line.split_once("...")?; + // PS2 prompts must have the same indentation as their + // corresponding PS1 prompt.[1] While the 'doctest' Python + // module will error in this case, we just treat this line as a + // non-doctest line. + // + // [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L733 + if ps1_indent != ps2_indent { + return None; + } + // PS2 prompts must be followed by an ASCII space character unless + // it's an otherwise empty line[1]. + // + // [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L809-L812 + match ps2_after.strip_prefix(' ') { + None if ps2_after.is_empty() => Some(""), + None => None, + Some(code) => Some(code), + } +} + +/// Formats the given source code using the given options. +/// +/// The given quote style should correspond to the style used by the docstring +/// containing the code snippet being formatted. The formatter will use this +/// information to invert the quote style of any such strings contained within +/// the code snippet in order to avoid writing invalid Python code. +/// +/// This is similar to the top-level formatting entrypoint, except this +/// explicitly sets the context to indicate that formatting is taking place +/// inside of a docstring. +fn docstring_format_source( + options: &crate::PyFormatOptions, + docstring_quote_style: QuoteStyle, + source: &str, +) -> Result { + use ruff_python_parser::AsMode; + + let source_type = options.source_type(); + let (tokens, comment_ranges) = ruff_python_index::tokens_and_ranges(source, source_type)?; + let module = + ruff_python_parser::parse_ok_tokens(tokens, source, source_type.as_mode(), "")?; + let source_code = ruff_formatter::SourceCode::new(source); + let comments = crate::Comments::from_ast(&module, source_code, &comment_ranges); + let locator = Locator::new(source); + + let ctx = PyFormatContext::new(options.clone(), locator.contents(), comments) + .in_docstring(docstring_quote_style); + let formatted = crate::format!(ctx, [module.format()])?; + formatted + .context() + .comments() + .assert_all_formatted(source_code); + Ok(formatted.print()?) +} + +/// 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(normalized: &NormalizedString, trim_end: &str) -> bool { + trim_end.ends_with(normalized.quotes.style.as_char()) + || trim_end.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 } #[cfg(test)] diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index d85806e3dfc63..e9a3c34757da9 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -15,7 +15,9 @@ use crate::comments::{ dangling_comments, leading_comments, trailing_comments, Comments, SourceComment, }; pub use crate::context::PyFormatContext; -pub use crate::options::{MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle}; +pub use crate::options::{ + DocstringCode, MagicTrailingComma, PreviewMode, PyFormatOptions, QuoteStyle, +}; pub use crate::shared_traits::{AsFormat, FormattedIter, FormattedIterExt, IntoFormat}; use crate::verbatim::suppressed_node; diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index b39c7b6ff79c6..261dfeab190ff 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -43,6 +43,12 @@ pub struct PyFormatOptions { /// in the formatted document. source_map_generation: SourceMapGeneration, + /// Whether to format code snippets in docstrings or not. + /// + /// By default this is disabled (opt-in), but the plan is to make this + /// enabled by default (opt-out) in the future. + docstring_code: DocstringCode, + /// Whether preview style formatting is enabled or not preview: PreviewMode, } @@ -70,6 +76,7 @@ impl Default for PyFormatOptions { line_ending: LineEnding::default(), magic_trailing_comma: MagicTrailingComma::default(), source_map_generation: SourceMapGeneration::default(), + docstring_code: DocstringCode::default(), preview: PreviewMode::default(), } } @@ -108,6 +115,10 @@ impl PyFormatOptions { self.line_ending } + pub fn docstring_code(&self) -> DocstringCode { + self.docstring_code + } + pub fn preview(&self) -> PreviewMode { self.preview } @@ -148,6 +159,12 @@ impl PyFormatOptions { self } + #[must_use] + pub fn with_docstring_code(mut self, docstring_code: DocstringCode) -> Self { + self.docstring_code = docstring_code; + self + } + #[must_use] pub fn with_preview(mut self, preview: PreviewMode) -> Self { self.preview = preview; @@ -284,3 +301,20 @@ impl PreviewMode { matches!(self, PreviewMode::Enabled) } } + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum DocstringCode { + #[default] + Disabled, + + Enabled, +} + +impl DocstringCode { + pub const fn is_enabled(self) -> bool { + matches!(self, DocstringCode::Enabled) + } +} diff --git a/crates/ruff_python_formatter/tests/fixtures.rs b/crates/ruff_python_formatter/tests/fixtures.rs index c3fd2b20707e8..1b949c7b73d0b 100644 --- a/crates/ruff_python_formatter/tests/fixtures.rs +++ b/crates/ruff_python_formatter/tests/fixtures.rs @@ -41,7 +41,7 @@ fn black_compatibility() { let formatted_code = printed.as_code(); ensure_unchanged_ast(&content, formatted_code, &options, input_path); - ensure_stability_when_formatting_twice(formatted_code, options, input_path); + ensure_stability_when_formatting_twice(formatted_code, &options, input_path); if formatted_code == expected_output { // Black and Ruff formatting matches. Delete any existing snapshot files because the Black output @@ -120,7 +120,7 @@ fn format() { let formatted_code = printed.as_code(); ensure_unchanged_ast(&content, formatted_code, &options, input_path); - ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path); + ensure_stability_when_formatting_twice(formatted_code, &options, input_path); let mut snapshot = format!("## Input\n{}", CodeFrame::new("python", &content)); @@ -138,7 +138,7 @@ fn format() { let formatted_code = printed.as_code(); ensure_unchanged_ast(&content, formatted_code, &options, input_path); - ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path); + ensure_stability_when_formatting_twice(formatted_code, &options, input_path); writeln!( snapshot, @@ -157,7 +157,7 @@ fn format() { let formatted_preview = printed_preview.as_code(); ensure_unchanged_ast(&content, formatted_preview, &options_preview, input_path); - ensure_stability_when_formatting_twice(formatted_preview, options_preview, input_path); + ensure_stability_when_formatting_twice(formatted_preview, &options_preview, input_path); if formatted_code == formatted_preview { writeln!( @@ -203,10 +203,10 @@ fn format() { /// Format another time and make sure that there are no changes anymore fn ensure_stability_when_formatting_twice( formatted_code: &str, - options: PyFormatOptions, + options: &PyFormatOptions, input_path: &Path, ) { - let reformatted = match format_module_source(formatted_code, options) { + let reformatted = match format_module_source(formatted_code, options.clone()) { Ok(reformatted) => reformatted, Err(err) => { panic!( @@ -223,7 +223,10 @@ fn ensure_stability_when_formatting_twice( .header("Formatted once", "Formatted twice") .to_string(); panic!( - r#"Reformatting the formatted code of {} a second time resulted in formatting changes. + r#"Reformatting the formatted code of {input_path} a second time resulted in formatting changes. + +Options: +{options} --- {diff}--- @@ -233,9 +236,10 @@ Formatted once: Formatted twice: --- -{}---"#, - input_path.display(), - reformatted.as_code(), +{reformatted}---"#, + input_path = input_path.display(), + options = &DisplayPyOptions(options), + reformatted = reformatted.as_code(), ); } } @@ -338,13 +342,17 @@ impl fmt::Display for DisplayPyOptions<'_> { line-width = {line_width} indent-width = {indent_width} quote-style = {quote_style:?} +line-ending = {line_ending:?} magic-trailing-comma = {magic_trailing_comma:?} +docstring-code = {docstring_code:?} preview = {preview:?}"#, indent_style = self.0.indent_style(), indent_width = self.0.indent_width().value(), line_width = self.0.line_width().value(), quote_style = self.0.quote_style(), + line_ending = self.0.line_ending(), magic_trailing_comma = self.0.magic_trailing_comma(), + docstring_code = self.0.docstring_code(), preview = self.0.preview() ) } diff --git a/crates/ruff_python_formatter/tests/normalizer.rs b/crates/ruff_python_formatter/tests/normalizer.rs index 991c4206fb9c2..d6f6454ed397a 100644 --- a/crates/ruff_python_formatter/tests/normalizer.rs +++ b/crates/ruff_python_formatter/tests/normalizer.rs @@ -1,4 +1,8 @@ -use itertools::Either::{Left, Right}; +use { + itertools::Either::{Left, Right}, + once_cell::sync::Lazy, + regex::Regex, +}; use ruff_python_ast::visitor::transformer; use ruff_python_ast::visitor::transformer::Transformer; @@ -12,6 +16,10 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; /// between `class C: ...` and `class C(): ...`, which is part of our AST but not `CPython`'s. /// - Normalize strings. The formatter can re-indent docstrings, so we need to compare string /// contents ignoring whitespace. (Black does the same.) +/// - The formatter can also reformat code snippets when they're Python code, which can of +/// course change the string in arbitrary ways. Black itself does not reformat code snippets, +/// so we carve our own path here by stripping everything that looks like code snippets from +/// string literals. /// - Ignores nested tuples in deletions. (Black does the same.) pub(crate) struct Normalizer; @@ -52,8 +60,28 @@ impl Transformer for Normalizer { } fn visit_string_literal(&self, string_literal: &mut ast::StringLiteral) { - // Normalize a string by (1) stripping any leading and trailing space from each - // line, and (2) removing any blank lines from the start and end of the string. + static STRIP_CODE_SNIPPETS: Lazy = Lazy::new(|| { + Regex::new( + r#"(?mx) + # strip doctest PS1 prompt lines + ^\s*>>>\s.*(\n|$) + | + # strip doctest PS2 prompt lines + # Also handles the case of an empty ... line. + ^\s*\.\.\.((\n|$)|\s.*(\n|$)) + "#, + ) + .unwrap() + }); + + // Start by (1) stripping everything that looks like a code + // snippet, since code snippets may be completely reformatted if + // they are Python code. + string_literal.value = STRIP_CODE_SNIPPETS + .replace_all(&string_literal.value, "") + .into_owned(); + // Normalize a string by (2) stripping any leading and trailing space from each + // line, and (3) removing any blank lines from the start and end of the string. string_literal.value = string_literal .value .lines() diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap index 4e92b7c09db29..bac8a446b18a1 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring.py.snap @@ -165,7 +165,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -331,7 +333,9 @@ indent-style = space line-width = 88 indent-width = 2 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -497,7 +501,9 @@ indent-style = tab line-width = 88 indent-width = 8 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -663,7 +669,9 @@ indent-style = tab line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap new file mode 100644 index 0000000000000..3a83075f29975 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap @@ -0,0 +1,3006 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.py +--- +## Input +```python +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + >>> cool_stuff( y ) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... + ... print( x ) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested( x ): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + ''' + Do cool stuff. + + >>> assert ("Easy!") + >>> import math + >>> math.floor( 1.9 ) + 1 + ''' + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + ''' + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard) + ''' + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( x ) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +preview = Disabled +``` + +```python +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + >>> cool_stuff( y ) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... + ... print( x ) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested( x ): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + """ + Do cool stuff. + + >>> assert ("Easy!") + >>> import math + >>> math.floor( 1.9 ) + 1 + """ + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + """ + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard) + """ + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line( + lion, giraffe, hippo, zeba, lemur, penguin, monkey + ) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( x ) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass +``` + + +### Output 2 +``` +indent-style = space +line-width = 88 +indent-width = 2 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +preview = Disabled +``` + +```python +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + >>> cool_stuff( y ) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... + ... print( x ) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested( x ): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + """ + Do cool stuff. + + >>> assert ("Easy!") + >>> import math + >>> math.floor( 1.9 ) + 1 + """ + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + """ + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard) + """ + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line( + lion, giraffe, hippo, zeba, lemur, penguin, monkey + ) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( x ) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass +``` + + +### Output 3 +``` +indent-style = tab +line-width = 88 +indent-width = 8 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +preview = Disabled +``` + +```python +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + >>> cool_stuff( y ) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... + ... print( x ) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested( x ): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + """ + Do cool stuff. + + >>> assert ("Easy!") + >>> import math + >>> math.floor( 1.9 ) + 1 + """ + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + """ + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard) + """ + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line( + lion, giraffe, hippo, zeba, lemur, penguin, monkey + ) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( x ) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass +``` + + +### Output 4 +``` +indent-style = tab +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +preview = Disabled +``` + +```python +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + >>> cool_stuff( y ) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... + ... print( x ) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested( x ): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + """ + Do cool stuff. + + >>> assert ("Easy!") + >>> import math + >>> math.floor( 1.9 ) + 1 + """ + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + """ + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard) + """ + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line( + lion, giraffe, hippo, zeba, lemur, penguin, monkey + ) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( x ) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass +``` + + +### Output 5 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Enabled +preview = Disabled +``` + +```python +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff(1) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(f"hi {x}") + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff(x) + >>> cool_stuff(y) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff(x) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(f"hi {x}") + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... + ... print(x) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print(x) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print(x) + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested(x): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + """ + Do cool stuff. + + >>> assert "Easy!" + >>> import math + >>> math.floor(1.9) + 1 + """ + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + """ + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line( + ... lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard + ... ) + """ + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line( + lion, giraffe, hippo, zeba, lemur, penguin, monkey + ) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass +``` + + +### Output 6 +``` +indent-style = space +line-width = 88 +indent-width = 2 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Enabled +preview = Disabled +``` + +```python +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff(1) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(f"hi {x}") + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff(x) + >>> cool_stuff(y) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff(x) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(f"hi {x}") + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... + ... print(x) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print(x) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print(x) + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested(x): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + """ + Do cool stuff. + + >>> assert "Easy!" + >>> import math + >>> math.floor(1.9) + 1 + """ + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + """ + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line( + ... lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard + ... ) + """ + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line( + lion, giraffe, hippo, zeba, lemur, penguin, monkey + ) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass +``` + + +### Output 7 +``` +indent-style = tab +line-width = 88 +indent-width = 8 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Enabled +preview = Disabled +``` + +```python +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff(1) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(f"hi {x}") + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff(x) + >>> cool_stuff(y) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff(x) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(f"hi {x}") + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... + ... print(x) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print(x) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print(x) + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested(x): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + """ + Do cool stuff. + + >>> assert "Easy!" + >>> import math + >>> math.floor(1.9) + 1 + """ + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + """ + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line( + ... lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard + ... ) + """ + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line( + lion, giraffe, hippo, zeba, lemur, penguin, monkey + ) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass +``` + + +### Output 8 +``` +indent-style = tab +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Enabled +preview = Disabled +``` + +```python +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. + + >>> cool_stuff(1) + 2 + """ + pass + + +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(f"hi {x}") + hi 2 + """ + pass + + +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + + >>> cool_stuff(x) + >>> cool_stuff(y) + 2 + """ + pass + + +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): + """ + Do cool stuff. + + >>> cool_stuff(x) + """ + pass + + +# Test that a doctest that continues to the last non-whitespace line of +# a docstring reformats correctly. +def doctest_last_line_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(f"hi {x}") + """ + pass + + +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... + ... print(x) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped. +# It is treated as part of the Python snippet which will trim the +# trailing whitespace. +def doctest_blank_end(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print(x) + """ + pass + + +# Tests that a blank PS2 line at the end of a doctest can get dropped +# even when there is text following it. +def doctest_blank_end_then_some_text(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print(x) + + And say something else. + """ + pass + + +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = '''tricksy''' + """ + pass + + +# Test that a doctest containing a triple quoted f-string gets +# formatted correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): + """ + Do cool stuff. + + >>> x = f'''tricksy''' + """ + pass + + +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): + """ + Do cool stuff. + + >>> x = '''\"\"\"''' + """ + pass + + +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. + + >>> x = """tricksy""" + ''' + pass + + +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. + + >>> x = f"""tricksy""" + ''' + pass + + +# Tests nested doctests are ignored. That is, we don't format doctests +# recursively. We only recognize "top level" doctests. +# +# This restriction primarily exists to avoid needing to deal with +# nesting quotes. It also seems like a generally sensible restriction, +# although it could be lifted if necessary I believe. +def doctest_nested_doctest_not_formatted(): + ''' + Do cool stuff. + + >>> def nested(x): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass + + +# Tests that the starting column does not matter. +def doctest_varying_start_column(): + """ + Do cool stuff. + + >>> assert "Easy!" + >>> import math + >>> math.floor(1.9) + 1 + """ + pass + + +# Tests that long lines get wrapped... appropriately. +# +# The docstring code formatter uses the same line width settings as for +# formatting other code. This means that a line in the docstring can +# actually extend past the configured line limit. +# +# It's not quite clear whether this is desirable or not. We could in +# theory compute the intendation length of a code snippet and then +# adjust the line-width setting on a recursive call to the formatter. +# But there are assuredly pathological cases to consider. Another path +# would be to expose another formatter option for controlling the +# line-width of code snippets independently. +def doctest_long_lines(): + """ + Do cool stuff. + + This won't get wrapped even though it exceeds our configured + line width because it doesn't exceed the line width within this + docstring. e.g, the `f` in `foo` is treated as the first column. + >>> foo, bar, quux = this_is_a_long_line(lion, giraffe, hippo, zeba, lemur, penguin, monkey) + + But this one is long enough to get wrapped. + >>> foo, bar, quux = this_is_a_long_line( + ... lion, giraffe, hippo, zeba, lemur, penguin, monkey, spider, bear, leopard + ... ) + """ + # This demostrates a normal line that will get wrapped but won't + # get wrapped in the docstring above because of how the line-width + # setting gets reset at the first column in each code snippet. + foo, bar, quux = this_is_a_long_line( + lion, giraffe, hippo, zeba, lemur, penguin, monkey + ) + + +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. + + >>> cool-stuff( x ): + 2 + """ + pass + + +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): + """ + Do cool stuff. + + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass + + +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with some proper indentation and some improper +# indentation is "partially" formatted. That is, the part that appears +# before the inconsistent indentation is formatted. This requires that +# the part before it is valid Python. +def doctest_skipped_partial_inconsistent_indent(): + """ + Do cool stuff. + + >>> def cool_stuff(x): + ... print(x) + ... print( f"hi {x}" ); + hi 2 + """ + pass + + +# Checks that a doctest with improper triple single quoted string gets +# skipped. That is, the code snippet is itself invalid Python, so it is +# left as is. +def doctest_skipped_triple_incorrect(): + """ + Do cool stuff. + + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass + + +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass + + +# f-strings are not considered docstrings[1], so any doctests +# inside of them should not be formatted. +# +# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +def doctest_skipped_fstring(): + f""" + Do cool stuff. + + >>> cool_stuff( 1 ) + 2 + """ + pass + + +# Test that a doctest containing a triple quoted string at least +# does not result in invalid Python code. Ideally this would format +# correctly, but at time of writing it does not. +def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): + """ + Do cool stuff. + + >>> x = '\"\"\"' + """ + pass +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap new file mode 100644 index 0000000000000..7391fb69387a6 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py +--- +## Input +```python +def doctest_line_ending(): + """ + Do cool stuff. + >>> def foo( x ): + ... print( x ) + ... + ... print( x ) + """ + pass +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = CarriageReturnLineFeed +magic-trailing-comma = Respect +docstring-code = Enabled +preview = Disabled +``` + +```python +def doctest_line_ending(): + """ + Do cool stuff. + >>> def foo(x): + ... print(x) + ... + ... print(x) + """ + pass +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap index 1e8baabd73238..ba906f5a34567 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__bytes.py.snap @@ -133,7 +133,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -282,7 +284,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Single +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap index 09830dae06dfa..af7a9ecb62130 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__string.py.snap @@ -148,7 +148,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -321,7 +323,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Single +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap index 8cca1bee8815a..cd2da8896be2c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__fmt_off_docstring.py.snap @@ -32,7 +32,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -65,7 +67,9 @@ indent-style = space line-width = 88 indent-width = 2 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap index 87873bcc8a203..12a58faa13871 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__indent.py.snap @@ -13,7 +13,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -27,7 +29,9 @@ indent-style = space line-width = 88 indent-width = 1 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -41,7 +45,9 @@ indent-style = tab line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap index 619cb49876d8d..54e970077c352 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@fmt_on_off__mixed_space_and_tab.py.snap @@ -28,7 +28,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -58,7 +60,9 @@ indent-style = space line-width = 88 indent-width = 2 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -88,7 +92,9 @@ indent-style = tab line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap index f7167deef5e09..c3504c47f856c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap @@ -79,7 +79,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -158,7 +160,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Enabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap index 1434f6a964a93..4add8537b1d2c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap @@ -46,7 +46,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -99,7 +101,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Ignore +docstring-code = Disabled preview = Disabled ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap index 2ff5d8afde9b6..d57334c7caf6e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@tab_width.py.snap @@ -21,7 +21,9 @@ indent-style = space line-width = 88 indent-width = 2 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -43,7 +45,9 @@ indent-style = space line-width = 88 indent-width = 4 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ``` @@ -68,7 +72,9 @@ indent-style = space line-width = 88 indent-width = 8 quote-style = Double +line-ending = LineFeed magic-trailing-comma = Respect +docstring-code = Disabled preview = Disabled ```