From d9845a2628d25cba3fa21bb53ad78ad377128aa2 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Mon, 27 Nov 2023 11:14:55 -0500 Subject: [PATCH] format doctests in docstrings (#8811) ## Summary This PR adds opt-in support for formatting doctests in docstrings. This reflects initial support and it is intended to add support for Markdown and reStructuredText Python code blocks in the future. But I believe this PR lays the groundwork, and future additions for Markdown and reST should be less costly to add. It's strongly recommended to review this PR commit-by-commit. The last few commits in particular implement the bulk of the work here and represent the denser portions. Some things worth mentioning: * The formatter is itself not perfect, and it is possible for it to produce invalid Python code. Because of this, reformatted code snippets are checked for Python validity. If they aren't valid, then we (unfortunately silently) bail on formatting that code snippet. * There are a couple places where it would be nice to at least warn the user that doctest formatting failed, but it wasn't clear to me what the best way to do that is. * I haven't yet run this in anger on a real world code base. I think that should happen before merging. Closes #7146 ## Test Plan * [x] Pass the local test suite. * [x] Scrutinize ecosystem changes. * [x] Run this formatter on extant code and scrutinize the results. (e.g., CPython, numpy.) --- .gitattributes | 3 + Cargo.lock | 1 + crates/ruff_python_formatter/Cargo.toml | 1 + .../ruff/docstring_code_examples.options.json | 42 + .../fixtures/ruff/docstring_code_examples.py | 316 ++ .../docstring_code_examples_crlf.options.json | 8 + .../ruff/docstring_code_examples_crlf.py | 9 + crates/ruff_python_formatter/src/context.rs | 33 +- .../src/expression/string.rs | 676 +++- crates/ruff_python_formatter/src/lib.rs | 4 +- crates/ruff_python_formatter/src/options.rs | 34 + .../ruff_python_formatter/tests/fixtures.rs | 28 +- .../ruff_python_formatter/tests/normalizer.rs | 34 +- .../tests/snapshots/format@docstring.py.snap | 8 + .../format@docstring_code_examples.py.snap | 3006 +++++++++++++++++ ...ormat@docstring_code_examples_crlf.py.snap | 44 + .../format@expression__bytes.py.snap | 4 + .../format@expression__string.py.snap | 4 + ...rmat@fmt_on_off__fmt_off_docstring.py.snap | 4 + .../format@fmt_on_off__indent.py.snap | 6 + ...at@fmt_on_off__mixed_space_and_tab.py.snap | 6 + .../tests/snapshots/format@preview.py.snap | 4 + .../format@skip_magic_trailing_comma.py.snap | 4 + .../tests/snapshots/format@tab_width.py.snap | 6 + 24 files changed, 4194 insertions(+), 91 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/docstring_code_examples_crlf.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@docstring_code_examples_crlf.py.snap 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 ```