From 02fa505d00c75d2a47c6874a2837a5371d6a7726 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 6 Dec 2023 14:48:26 -0500 Subject: [PATCH] ruff_python_formatter: support reformatting Markdown code blocks This commit slots in support for formatting Markdown fenced code blocks[1]. With the refactoring done for reStructuredText previously, this ended up being pretty easy to add. Markdown code blocks are also quite a bit easier to parse and recognize correctly. One point of contention in #8860 is whether to assume that unlabeled Markdown code fences are Python or not by default. In this PR, we make such an assumption. This follows what `rustdoc` does. The mitigation here is that if an unlabeled code block isn't Python, then it probably won't parse as Python. And we'll end up skipping it. So in the vast majority of cases, the worst thing that can happen is a little bit of wasted work. Closes #8860 [1]: https://spec.commonmark.org/0.30/#fenced-code-blocks --- .../fixtures/ruff/docstring_code_examples.py | 506 + .../src/expression/string/docstring.rs | 251 + .../ruff_python_formatter/tests/normalizer.rs | 10 + .../format@docstring_code_examples.py.snap | 12668 +++++++++++----- 4 files changed, 9362 insertions(+), 4073 deletions(-) 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 index 296da3d816545e..68c7a528a6cf87 100644 --- 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 @@ -795,6 +795,19 @@ def rst_literal_skipped_doctest(): pass +def rst_literal_skipped_markdown(): + """ + Do cool stuff:: + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + def rst_directive_skipped_not_indented(): """ .. code-block:: python @@ -828,3 +841,496 @@ def rst_directive_skipped_doctest(): Done. """ pass + + +############################################################################### +# Markdown CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Markdown fenced code blocks. +# +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks +############################################################################### + + +def markdown_simple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_simple_continued(): + """ + Do cool stuff. + + ```python + def cool_stuff( x ): + print( f"hi {x}" ); + ``` + + Done. + """ + pass + + +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): + """ + Do cool stuff. + + ``` + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): + """ + Do cool stuff. + + ~~~py + cool_stuff( 1 ) + ~~~ + + Done. + """ + pass + + +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + `````` + + Done. + """ + pass + + +# Tests that an invalid closing fence is treated as invalid. +# +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ''' + ```invalid + ''' + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): + """ + Do cool stuff. + + `````` + do_something( ''' + ``` + did i trick you? + ``` + ''' ) + `````` + + Done. + """ + pass + + +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + + + + """ + pass + + +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + """ + pass + + +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + """ + pass + + +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ```""" + pass + + +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 )""" + pass + + +def markdown_with_blank_lines(): + """ + Do cool stuff. + + ```py + def cool_stuff( x ): + print( f"hi {x}" ); + + def other_stuff( y ): + print( y ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_tab_second_line_spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_odd_indentation(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): + """ + Do cool stuff. + + ```py + + + cool_stuff( 1 ) + + + ``` + + Done. + """ + pass + + +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): + """ + Do cool stuff. + + ```py + + cool_stuff( 1 ) + + + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_weird_closing(): + """ + Code block with weirdly placed closing fences. + + ```python + cool_stuff( 1 ) + + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` + + Now the code block is closed + """ + pass + + +def markdown_over_indented(): + """ + A docstring + over intended + ```python + print( 5 ) + ``` + """ + pass + + +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. + """ + pass + + +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` + + Done. + """ + pass + + +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. +# +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): + """ + Do cool stuff. + + ```py +cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. +# +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_skipped_doctest(): + """ + Do cool stuff. + + ```py + >>> cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_skipped_rst_literal(): + """ + Do cool stuff. + + ```py + And do this:: + + cool_stuff( 1 ) + + ``` + + Done. + """ + pass + + +def markdown_skipped_rst_directive(): + """ + Do cool stuff. + + ```py + .. code-block:: python + + cool_stuff( 1 ) + + ``` + + Done. + """ + pass diff --git a/crates/ruff_python_formatter/src/expression/string/docstring.rs b/crates/ruff_python_formatter/src/expression/string/docstring.rs index c90fa4480b6352..d030543c7c9c6e 100644 --- a/crates/ruff_python_formatter/src/expression/string/docstring.rs +++ b/crates/ruff_python_formatter/src/expression/string/docstring.rs @@ -354,6 +354,16 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { )?; } } + CodeExampleKind::Markdown(fenced) => { + // This looks suspicious, but it's consistent with the whitespace + // normalization that will occur anyway. + let indent = " ".repeat(fenced.opening_fence_indent.to_usize()); + for docline in formatted_lines { + self.print_one( + &docline.map(|line| std::format!("{indent}{line}")), + )?; + } + } } } } @@ -648,6 +658,18 @@ impl<'src> CodeExample<'src> { }; self.kind = Some(CodeExampleKind::Rst(litblock)); } + Some(CodeExampleKind::Markdown(fenced)) => { + let Some(fenced) = fenced.add_code_line(original, queue) else { + // For Markdown, the last line in a block should be printed + // as-is. Especially since the last line in many Markdown + // fenced code blocks is identical to the start of a code + // block. So if we try to start a new code block with + // the last line, we risk opening another Markdown block + // inappropriately. + return; + }; + self.kind = Some(CodeExampleKind::Markdown(fenced)); + } } } @@ -681,6 +703,9 @@ impl<'src> CodeExample<'src> { } else if let Some(litblock) = CodeExampleRst::new(original) { self.kind = Some(CodeExampleKind::Rst(litblock)); queue.push_back(CodeExampleAddAction::Print { original }); + } else if let Some(fenced) = CodeExampleMarkdown::new(original) { + self.kind = Some(CodeExampleKind::Markdown(fenced)); + queue.push_back(CodeExampleAddAction::Print { original }); } else { queue.push_back(CodeExampleAddAction::Print { original }); } @@ -707,6 +732,10 @@ enum CodeExampleKind<'src> { /// [literal block]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks /// [code block directive]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block Rst(CodeExampleRst<'src>), + /// Code found from a Markdown "[fenced code block]". + /// + /// [fenced code block]: https://spec.commonmark.org/0.30/#fenced-code-blocks + Markdown(CodeExampleMarkdown<'src>), } impl<'src> CodeExampleKind<'src> { @@ -718,6 +747,7 @@ impl<'src> CodeExampleKind<'src> { match *self { CodeExampleKind::Doctest(ref doctest) => &doctest.lines, CodeExampleKind::Rst(ref mut litblock) => litblock.indented_code(), + CodeExampleKind::Markdown(ref fenced) => &fenced.lines, } } @@ -731,6 +761,7 @@ impl<'src> CodeExampleKind<'src> { match self { CodeExampleKind::Doctest(doctest) => doctest.lines, CodeExampleKind::Rst(litblock) => litblock.lines, + CodeExampleKind::Markdown(fenced) => fenced.lines, } } } @@ -1156,6 +1187,226 @@ impl<'src> CodeExampleRst<'src> { } } +/// Represents a code example extracted from a Markdown [fenced code block]. +/// +/// [fenced code block]: https://spec.commonmark.org/0.30/#fenced-code-blocks +#[derive(Debug)] +struct CodeExampleMarkdown<'src> { + /// The lines that have been seen so far that make up the block. + lines: Vec>, + + /// The indent of the line "opening" fence of this block measured via + /// `indentation_length`. + /// + /// This indentation is trimmed from the indentation of every line in the + /// body of the code block, + opening_fence_indent: TextSize, + + /// The kind of fence, backticks or tildes, used for this block. We need to + /// keep track of which kind was used to open the block in order to look + /// for a correct close of the block. + fence_kind: MarkdownFenceKind, + + /// The size of the fence, in codepoints, in the opening line. A correct + /// close of the fence must use *at least* this many characters. In other + /// words, this is the number of backticks or tildes that opened the fenced + /// code block. + fence_len: usize, +} + +impl<'src> CodeExampleMarkdown<'src> { + /// Looks for the start of a Markdown [fenced code block]. + /// + /// If the start of a block is found, then this returns a correctly + /// initialized Markdown code block. Callers should print the line as given + /// as it is not retained as part of the block. + /// + /// [fenced code block]: https://spec.commonmark.org/0.30/#fenced-code-blocks + fn new(original: InputDocstringLine<'src>) -> Option> { + let (opening_fence_indent, rest) = indent_with_suffix(original.line); + // Quit quickly in the vast majority of cases. + if !rest.starts_with("```") && !rest.starts_with("~~~") { + return None; + } + + static FENCE_START: Lazy = Lazy::new(|| { + Regex::new( + r"(?xm) + ^ + (?: + # In the backtick case, info strings (following the fence) + # cannot contain backticks themselves, since it would + # introduce ambiguity with parsing inline code. In other + # words, if we didn't specifically exclude matching ` + # in the info string for backtick fences, then we might + # erroneously consider something to be a code fence block + # that is actually inline code. + # + # NOTE: The `ticklang` and `tildlang` capture groups are + # currently unused, but there was some discussion about not + # assuming unlabeled blocks were Python. At the time of + # writing, we do assume unlabeled blocks are Python, but + # one could inspect the `ticklang` and `tildlang` capture + # groups to determine whether the block is labeled or not. + (?```+)(?:\s*(?(?i:python|py|python3|py3))[^`]*)? + | + (?~~~+)(?:\s*(?(?i:python|py|python3|py3))\p{any}*)? + ) + $ + ", + ) + .unwrap() + }); + let caps = FENCE_START.captures(rest)?; + let (fence_kind, fence_len) = if let Some(ticks) = caps.name("ticks") { + (MarkdownFenceKind::Backtick, ticks.as_str().chars().count()) + } else { + let tildes = caps + .name("tilds") + .expect("no ticks means it must be tildes"); + (MarkdownFenceKind::Tilde, tildes.as_str().chars().count()) + }; + Some(CodeExampleMarkdown { + lines: vec![], + opening_fence_indent: indentation_length(opening_fence_indent), + fence_kind, + fence_len, + }) + } + + /// Attempts to add the given line from a docstring to the Markdown code + /// snippet being collected. + /// + /// In this case, ownership is only not returned when the end of the block + /// was found, or if the block was determined to be invalid. A formatting + /// action is then pushed onto the queue. + fn add_code_line( + mut self, + original: InputDocstringLine<'src>, + queue: &mut VecDeque>, + ) -> Option> { + if self.is_end(original) { + queue.push_back(self.into_format_action()); + queue.push_back(CodeExampleAddAction::Print { original }); + return None; + } + // When a line in a Markdown fenced closed block is indented *less* + // than the opening indent, we treat the entire block as invalid. + // + // I believe that code blocks of this form are actually valid Markdown + // in some cases, but the interplay between it and our docstring + // whitespace normalization leads to undesirable outcomes. For example, + // if the line here is unindented out beyond the initial indent of the + // docstring itself, then this causes the entire docstring to have + // its indent normalized. And, at the time of writing, a subsequent + // formatting run undoes this indentation, thus violating idempotency. + if !original.line.trim_whitespace().is_empty() + && indentation_length(original.line) < self.opening_fence_indent + { + queue.push_back(self.into_reset_action()); + queue.push_back(CodeExampleAddAction::Print { original }); + return None; + } + self.push(original); + queue.push_back(CodeExampleAddAction::Kept); + Some(self) + } + + /// Returns true when given line ends this fenced code block. + fn is_end(&self, original: InputDocstringLine<'src>) -> bool { + let (_, rest) = indent_with_suffix(original.line); + // We can bail early if we don't have at least three backticks or + // tildes. + if !rest.starts_with("```") && !rest.starts_with("~~~") { + return false; + } + // We do need to check that we have the right number of + // backticks/tildes... + let fence_len = rest + .chars() + .take_while(|&ch| ch == self.fence_kind.to_char()) + .count(); + // A closing fence only needs *at least* the number of ticks/tildes + // that are in the opening fence. + if fence_len < self.fence_len { + return false; + } + // And, also, there can only be trailing whitespace. Nothing else. + assert!( + self.fence_kind.to_char().is_ascii(), + "fence char should be ASCII", + ); + if !rest[fence_len..].chars().all(is_python_whitespace) { + return false; + } + true + } + + /// Pushes the given line as part of this code example. + fn push(&mut self, original: InputDocstringLine<'src>) { + // Unlike reStructuredText blocks, for Markdown fenced code blocks, the + // indentation that we want to strip from each line is known when the + // block is opened. So we can strip it as we collect lines. + let code = indentation_trim(self.opening_fence_indent, original.line); + self.lines.push(CodeExampleLine { original, code }); + } + + /// Consume this block and turn it into a reset action. + /// + /// This occurs when we started collecting a code example from something + /// that looked like a block, but later determined that it wasn't a valid + /// block. + fn into_format_action(self) -> CodeExampleAddAction<'src> { + // Note that unlike in reStructuredText blocks, if a Markdown fenced + // code block is unclosed, then *all* remaining lines should be treated + // as part of the block[1]: + // + // > If the end of the containing block (or document) is reached and no + // > closing code fence has been found, the code block contains all of the + // > lines after the opening code fence until the end of the containing + // > block (or document). + // + // This means that we don't need to try and trim trailing empty lines. + // Those will get fed into the code formatter and ultimately stripped, + // which is what you'd expect if those lines are treated as part of the + // block. + // + // [1]: https://spec.commonmark.org/0.30/#fenced-code-blocks + CodeExampleAddAction::Format { + kind: CodeExampleKind::Markdown(self), + } + } + + /// Consume this block and turn it into a reset action. + /// + /// This occurs when we started collecting a code example from something + /// that looked like a code fence, but later determined that it wasn't a + /// valid. + fn into_reset_action(self) -> CodeExampleAddAction<'src> { + CodeExampleAddAction::Reset { code: self.lines } + } +} + +/// The kind of fence used in a Markdown code block. +/// +/// This indicates that the fence is either surrounded by fences made from +/// backticks, or fences made from tildes. +#[derive(Clone, Copy, Debug)] +enum MarkdownFenceKind { + Backtick, + Tilde, +} + +impl MarkdownFenceKind { + /// Convert the fence kind to the actual character used to build the fence. + fn to_char(&self) -> char { + match *self { + MarkdownFenceKind::Backtick => '`', + MarkdownFenceKind::Tilde => '~', + } + } +} + /// A single line in a code example found in a docstring. /// /// A code example line exists prior to formatting, and is thus in full diff --git a/crates/ruff_python_formatter/tests/normalizer.rs b/crates/ruff_python_formatter/tests/normalizer.rs index 8f01694468dcbd..2bab8915cc0543 100644 --- a/crates/ruff_python_formatter/tests/normalizer.rs +++ b/crates/ruff_python_formatter/tests/normalizer.rs @@ -82,6 +82,10 @@ impl Transformer for Normalizer { // everything after it. Talk about a hammer. Regex::new(r#"::(?s:.*)"#).unwrap() }); + static STRIP_MARKDOWN_BLOCKS: Lazy = Lazy::new(|| { + // This covers more than valid Markdown blocks, but that's OK. + Regex::new(r#"(```|~~~)\p{any}*(```|~~~|$)"#).unwrap() + }); // Start by (1) stripping everything that looks like a code // snippet, since code snippets may be completely reformatted if @@ -98,6 +102,12 @@ impl Transformer for Normalizer { "\n", ) .into_owned(); + string_literal.value = STRIP_MARKDOWN_BLOCKS + .replace_all( + &string_literal.value, + "\n", + ) + .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 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 index cae50ad8c6d91d..7dc1badfe2b8a5 100644 --- 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 @@ -801,6 +801,19 @@ def rst_literal_skipped_doctest(): pass +def rst_literal_skipped_markdown(): + """ + Do cool stuff:: + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + def rst_directive_skipped_not_indented(): """ .. code-block:: python @@ -834,1716 +847,5931 @@ def rst_directive_skipped_doctest(): Done. """ 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 ############################################################################### -# DOCTEST CODE EXAMPLES +# Markdown CODE EXAMPLES # # This section shows examples of docstrings that contain code snippets in -# Python's "doctest" format. +# Markdown fenced code blocks. # -# See: https://docs.python.org/3/library/doctest.html +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks ############################################################################### -# The simplest doctest to ensure basic formatting works. -def doctest_simple(): + +def markdown_simple(): """ Do cool stuff. - >>> cool_stuff( 1 ) - 2 + ```py + cool_stuff( 1 ) + ``` + + Done. """ pass -# Another simple test, but one where the Python code -# extends over multiple lines. -def doctest_simple_continued(): +def markdown_simple_continued(): """ Do cool stuff. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 + ```python + def cool_stuff( x ): + print( f"hi {x}" ); + ``` + + Done. """ pass -# Test that we support multiple directly adjacent -# doctests. -def doctest_adjacent(): +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): """ Do cool stuff. - >>> cool_stuff( x ) - >>> cool_stuff( y ) - 2 + ``` + cool_stuff( 1 ) + ``` + + Done. """ pass -# Test that a doctest on the last non-whitespace line of a docstring -# reformats correctly. -def doctest_last_line(): +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): """ Do cool stuff. - >>> cool_stuff( x ) + ~~~py + cool_stuff( 1 ) + ~~~ + + Done. """ pass -# Test that a doctest that continues to the last non-whitespace line of -# a docstring reformats correctly. -def doctest_last_line_continued(): +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): """ Do cool stuff. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); + ```py + cool_stuff( 1 ) + `````` + + Done. """ pass -# Test that a doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line(): +# Tests that an invalid closing fence is treated as invalid. +# +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): """ Do cool stuff. - >>> cool_stuff( x )""" + ```py + cool_stuff( 1 ) + ''' + ```invalid + ''' + cool_stuff( 2 ) + ``` + + Done. + """ pass -# Test that a continued doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line_continued(): +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): """ Do cool stuff. - >>> cool_stuff( x ) - ... more( y )""" + `````` + do_something( ''' + ``` + did i trick you? + ``` + ''' ) + `````` + + Done. + """ pass -# Test that a doctest is correctly identified and formatted with a blank -# continuation line. -def doctest_blank_continued(): +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): """ Do cool stuff. - >>> def cool_stuff ( x ): - ... print( x ) - ... - ... print( x ) + ```py + cool_stuff( 1 ) + + + """ 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(): +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): """ Do cool stuff. - >>> def cool_stuff ( x ): - ... print( x ) - ... print( x ) - ... + ```py + cool_stuff( 1 ) + ``` """ 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(): +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): """ Do cool stuff. - >>> def cool_stuff ( x ): - ... print( x ) - ... print( x ) - ... - - And say something else. + ```py + cool_stuff( 1 ) """ 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(): +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): """ Do cool stuff. - >>> x = '''tricksy''' - """ + ```py + cool_stuff( 1 ) + ```""" 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(): +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): """ Do cool stuff. - >>> x = f'''tricksy''' - """ + ```py + cool_stuff( 1 )""" 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(): +def markdown_with_blank_lines(): """ Do cool stuff. - >>> x = '''\"\"\"''' + ```py + def cool_stuff( x ): + print( f"hi {x}" ); + + def other_stuff( y ): + print( y ) + ``` + + Done. """ pass -# Tests that inverting the triple quoting works as expected. -def doctest_with_triple_inverted(): - ''' +def markdown_first_line_indent_uses_tabs_4spaces(): + """ Do cool stuff. - >>> x = """tricksy""" - ''' + ```py + cool_stuff( 1 ) + ``` + + Done. + """ pass -# Tests that inverting the triple quoting with an f-string works as -# expected. -def doctest_with_triple_inverted_fstring(): - ''' +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): + """ Do cool stuff. - >>> x = f"""tricksy""" - ''' + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ 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 markdown_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff. - >>> def nested( x ): - ... """ - ... Do nested cool stuff. - ... >>> func_call( 5 ) - ... """ - ... pass - ''' - pass + ```py + cool_stuff( 1 ) + ``` + Done. + """ + pass -# Tests that the starting column does not matter. -def doctest_varying_start_column(): + +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_tab_second_line_spaces(): """ Do cool stuff. - >>> assert ("Easy!") - >>> import math - >>> math.floor( 1.9 ) - 1 + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. """ 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(): +def markdown_odd_indentation(): """ 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) + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` - 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) + Done. """ - # 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 - ) + pass -# Checks that a simple but invalid doctest gets skipped. -def doctest_skipped_simple(): +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): """ Do cool stuff. - >>> cool-stuff( x ): - 2 + ```py + + + cool_stuff( 1 ) + + + ``` + + Done. """ pass -# Checks that a simple doctest that is continued over multiple lines, -# but is invalid, gets skipped. -def doctest_skipped_simple_continued(): +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): """ Do cool stuff. - >>> def cool-stuff( x ): - ... print( f"hi {x}" ); - 2 + ```py + + cool_stuff( 1 ) + + + cool_stuff( 2 ) + ``` + + Done. """ pass -# Checks that a doctest with improper indentation gets skipped. -def doctest_skipped_inconsistent_indent(): +def markdown_weird_closing(): """ - Do cool stuff. - - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 - """ - pass + Code block with weirdly placed closing fences. + ```python + cool_stuff( 1 ) -# 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. + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` - >>> def cool_stuff( x ): - ... print( x ) - ... print( f"hi {x}" ); - hi 2 + Now the code block is closed """ 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(): +def markdown_over_indented(): """ - Do cool stuff. - - >>> foo( x ) - ... '''tri'''cksy''' + A docstring + over intended + ```python + print( 5 ) + ``` """ 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""" +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): + """ Do cool stuff. - >>> cool_stuff( 1 ) - 2 + ```py + cool_stuff( 1 ) + + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. """ 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(): +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): """ Do cool stuff. - >>> x = '\"\"\"' + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` + + Done. """ pass -############################################################################### -# reStructuredText CODE EXAMPLES -# -# This section shows examples of docstrings that contain code snippets in -# reStructuredText formatted code blocks. +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. # -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 -############################################################################### - - -def rst_literal_simple(): +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff( 1 ) + ```py +cool_stuff( 1 ) + ``` Done. """ pass -def rst_literal_simple_continued(): +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): """ - Do cool stuff:: + Do cool stuff. - def cool_stuff( x ): - print( f"hi {x}" ); + ```py + cool_stuff( 1 ) + ``` Done. """ pass -# Tests that we can end the literal block on the second -# to last line of the docstring. -def rst_literal_second_to_last(): - """ - Do cool stuff:: - - cool_stuff( 1 ) +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. +# +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): """ - pass + Do cool stuff. + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` -# Tests that we can end the literal block on the actual -# last line of the docstring. -def rst_literal_actually_last(): + Done. """ - Do cool stuff:: - - cool_stuff( 1 )""" pass -def rst_literal_with_blank_lines(): +def markdown_skipped_doctest(): """ - Do cool stuff:: - - def cool_stuff( x ): - print( f"hi {x}" ); + Do cool stuff. - def other_stuff( y ): - print( y ) + ```py + >>> cool_stuff( 1 ) + ``` Done. """ pass -# Extra blanks should be preserved. -def rst_literal_extra_blanks(): +def markdown_skipped_rst_literal(): """ - Do cool stuff:: - + Do cool stuff. + ```py + And do this:: cool_stuff( 1 ) - + ``` Done. """ pass -# If a literal block is never properly ended (via a non-empty unindented line), -# then the end of the block should be the last non-empty line. And subsequent -# empty lines should be preserved as-is. -def rst_literal_extra_blanks_at_end(): +def markdown_skipped_rst_directive(): """ - Do cool stuff:: + Do cool stuff. + ```py + .. code-block:: python cool_stuff( 1 ) + ``` - + Done. """ 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 +``` -# A literal block can contain many empty lines and it should not end the block -# if it continues. -def rst_literal_extra_blanks_in_snippet(): - """ - Do cool stuff:: - - cool_stuff( 1 ) - +```python +############################################################################### +# DOCTEST CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Python's "doctest" format. +# +# See: https://docs.python.org/3/library/doctest.html +############################################################################### - cool_stuff( 2 ) +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. - Done. + >>> cool_stuff( 1 ) + 2 """ pass -# This tests that a unindented line appearing after an indented line (but where -# the indent is still beyond the minimum) gets formatted properly. -def rst_literal_subsequent_line_not_indented(): +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): """ - Do cool stuff:: - - if True: - cool_stuff( ''' - hiya''' ) + Do cool stuff. - Done. + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 """ pass -# This checks that if the first line in a code snippet has been indented with -# tabs, then so long as its "indentation length" is considered bigger than the -# line with `::`, it is reformatted as code. -# -# (If your tabwidth is set to 4, then it looks like the code snippet -# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST -# itself also seems to recognize this as a code block, although it appears -# under-specified.) -def rst_literal_first_line_indent_uses_tabs_4spaces(): +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): """ - Do cool stuff:: - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> cool_stuff( x ) + >>> cool_stuff( y ) + 2 """ pass -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): """ - Do cool stuff:: - - cool_stuff( 1 ) - cool_stuff( 2 ) + Do cool stuff. - Done. + >>> cool_stuff( x ) """ pass -# Another test with tabs, except in this case, if your tabwidth is less than -# 8, than the code snippet actually looks like its indent is *less* than the -# opening line with a `::`. One might presume this means that the code snippet -# is not treated as a literal block and thus not reformatted, but since we -# assume all tabs have tabwidth=8 when computing indentation length, the code -# snippet is actually seen as being more indented than the opening `::` line. -# As with the above example, reST seems to behave the same way here. -def rst_literal_first_line_indent_uses_tabs_8spaces(): +# 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:: - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); """ pass -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): +# Test that a doctest on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line(): """ - Do cool stuff:: - - cool_stuff( 1 ) - cool_stuff( 2 ) + Do cool stuff. - Done. - """ + >>> cool_stuff( x )""" pass -# Tests that if two lines in a literal block are indented to the same level -# but by different means (tabs versus spaces), then we correctly recognize the -# block and format it. -def rst_literal_first_line_tab_second_line_spaces(): +# Test that a continued doctest on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line_continued(): """ - Do cool stuff:: - - cool_stuff( 1 ) - cool_stuff( 2 ) + Do cool stuff. - Done. - """ + >>> cool_stuff( x ) + ... more( y )""" pass -# Tests that when two lines in a code snippet have weird and inconsistent -# indentation, the code still gets formatted so long as the indent is greater -# than the indent of the `::` line. -# -# In this case, the minimum indent is 5 spaces (from the second line) where as -# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). -# The minimum indent is stripped from each code line. Since tabs aren't -# divisible, the entire tab is stripped, which means the first and second lines -# wind up with the same level of indentation. -# -# An alternative behavior here would be that the tab is replaced with 3 spaces -# instead of being stripped entirely. The code snippet itself would then have -# inconsistent indentation to the point of being invalid Python, and thus code -# formatting would be skipped. -# -# I decided on the former behavior because it seems a bit easier to implement, -# but we might want to switch to the alternative if cases like this show up in -# the real world. ---AG -def rst_literal_odd_indentation(): +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): """ - Do cool stuff:: - - cool_stuff( 1 ) - cool_stuff( 2 ) + Do cool stuff. - Done. + >>> def cool_stuff ( x ): + ... print( x ) + ... + ... print( x ) """ pass -# Tests that having a line with a lone `::` works as an introduction of a -# literal block. -def rst_literal_lone_colon(): +# 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. - :: - - cool_stuff( 1 ) - - Done. + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... """ pass -def rst_directive_simple(): +# 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(): """ - .. code-block:: python + Do cool stuff. - cool_stuff( 1 ) + >>> def cool_stuff ( x ): + ... print( x ) + ... print( x ) + ... - Done. + And say something else. """ pass -def rst_directive_case_insensitive(): +# Test that a doctest containing a triple quoted string gets formatted +# correctly and doesn't result in invalid syntax. +def doctest_with_triple_single(): """ - .. cOdE-bLoCk:: python - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> x = '''tricksy''' """ pass -def rst_directive_sourcecode(): +# 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(): """ - .. sourcecode:: python - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> x = f'''tricksy''' """ pass -def rst_directive_options(): +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): """ - .. code-block:: python - :linenos: - :emphasize-lines: 2,3 - :name: blah blah - - cool_stuff( 1 ) - cool_stuff( 2 ) - cool_stuff( 3 ) - cool_stuff( 4 ) + Do cool stuff. - Done. + >>> x = '''\"\"\"''' """ pass -# In this case, since `pycon` isn't recognized as a Python code snippet, the -# docstring reformatter ignores it. But it then picks up the doctest and -# reformats it. -def rst_directive_doctest(): - """ - .. code-block:: pycon - - >>> cool_stuff( 1 ) +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. - Done. - """ + >>> x = """tricksy""" + ''' pass -# This checks that if the first non-empty line after the start of a literal -# block is not indented more than the line containing the `::`, then it is not -# treated as a code snippet. -def rst_literal_skipped_first_line_not_indented(): - """ - Do cool stuff:: - - cool_stuff( 1 ) +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. - Done. - """ + >>> x = f"""tricksy""" + ''' pass -# Like the test above, but inserts an indented line after the un-indented one. -# This should not cause the literal block to be resumed. -def rst_literal_skipped_first_line_not_indented_then_indented(): - """ - Do cool stuff:: - - cool_stuff( 1 ) - cool_stuff( 2 ) +# 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. - Done. - """ + >>> def nested( x ): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' pass -# This also checks that a code snippet is not reformatted when the indentation -# of the first line is not more than the line with `::`, but this uses tabs to -# make it a little more confounding. It relies on the fact that indentation -# length is computed by assuming a tabwidth equal to 8. reST also rejects this -# and doesn't treat it as a literal block. -def rst_literal_skipped_first_line_not_indented_tab(): +# Tests that the starting column does not matter. +def doctest_varying_start_column(): """ - Do cool stuff:: - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> assert ("Easy!") + >>> import math + >>> math.floor( 1.9 ) + 1 """ pass -# Like the previous test, but adds a second line. -def rst_literal_skipped_first_line_not_indented_tab_multiple(): +# 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:: + Do cool stuff. - cool_stuff( 1 ) - cool_stuff( 2 ) + 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) - Done. + 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) """ - pass + # 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 + ) -# Tests that a code block with a second line that is not properly indented gets -# skipped. A valid code block needs to have an empty line separating these. -# -# One trick here is that we need to make sure the Python code in the snippet is -# valid, otherwise it would be skipped because of invalid Python. -def rst_literal_skipped_subsequent_line_not_indented(): +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): """ - Do cool stuff:: - - if True: - cool_stuff( ''' - hiya''' ) + Do cool stuff. - Done. + >>> cool-stuff( x ): + 2 """ pass -# In this test, we write what looks like a code-block, but it should be treated -# as invalid due to the missing `language` argument. -# -# It does still look like it could be a literal block according to the literal -# rules, but we currently consider the `.. ` prefix to indicate that it is not -# a literal block. -def rst_literal_skipped_not_directive(): +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): """ - .. code-block:: - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 """ pass -# In this test, we start a line with `.. `, which makes it look like it might -# be a directive. But instead continue it as if it was just some periods from -# the previous line, and then try to end it by starting a literal block. -# -# But because of the `.. ` in the beginning, we wind up not treating this as a -# code snippet. The reST render I was using to test things does actually treat -# this as a code block, so we may be out of conformance here. -def rst_literal_skipped_possible_false_negative(): +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): """ - This is a test. - .. This is a test:: - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 """ pass -# This tests that a doctest inside of a reST literal block doesn't get -# reformatted. It's plausible this isn't the right behavior, but it also seems -# like it might be the right behavior since it is a literal block. (The doctest -# makes the Python code invalid.) -def rst_literal_skipped_doctest(): +# 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:: - - >>> cool_stuff( 1 ) + Do cool stuff. - Done. + >>> def cool_stuff( x ): + ... print( x ) + ... print( f"hi {x}" ); + hi 2 """ pass -def rst_directive_skipped_not_indented(): +# 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(): """ - .. code-block:: python - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> foo( x ) + ... '''tri'''cksy''' """ pass -def rst_directive_skipped_wrong_language(): - """ - .. code-block:: rust +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass - cool_stuff( 1 ) - Done. +# 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 -# This gets skipped for the same reason that the doctest in a literal block -# gets skipped. -def rst_directive_skipped_doctest(): +# 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(): """ - .. code-block:: python - - >>> cool_stuff( 1 ) + Do cool stuff. - Done. + >>> 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 ############################################################################### -# DOCTEST CODE EXAMPLES +# reStructuredText CODE EXAMPLES # # This section shows examples of docstrings that contain code snippets in -# Python's "doctest" format. +# reStructuredText formatted code blocks. # -# See: https://docs.python.org/3/library/doctest.html +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 ############################################################################### -# The simplest doctest to ensure basic formatting works. -def doctest_simple(): - """ - Do cool stuff. - - >>> cool_stuff( 1 ) - 2 - """ - pass +def rst_literal_simple(): + """ + Do cool stuff:: -# Another simple test, but one where the Python code -# extends over multiple lines. -def doctest_simple_continued(): - """ - Do cool stuff. + cool_stuff( 1 ) - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 - """ - pass + Done. + """ + pass -# Test that we support multiple directly adjacent -# doctests. -def doctest_adjacent(): - """ - Do cool stuff. +def rst_literal_simple_continued(): + """ + Do cool stuff:: - >>> cool_stuff( x ) - >>> cool_stuff( y ) - 2 - """ - pass + def cool_stuff( x ): + print( f"hi {x}" ); + Done. + """ + 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 +# Tests that we can end the literal block on the second +# to last line of the docstring. +def rst_literal_second_to_last(): + """ + Do cool stuff:: + cool_stuff( 1 ) + """ + 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 +# Tests that we can end the literal block on the actual +# last line of the docstring. +def rst_literal_actually_last(): + """ + Do cool stuff:: + cool_stuff( 1 )""" + pass -# Test that a doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line(): - """ - Do cool stuff. - >>> cool_stuff( x )""" - pass +def rst_literal_with_blank_lines(): + """ + Do cool stuff:: + def cool_stuff( x ): + print( f"hi {x}" ); -# Test that a continued doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line_continued(): - """ - Do cool stuff. + def other_stuff( y ): + print( y ) - >>> cool_stuff( x ) - ... more( y )""" - pass + Done. + """ + pass -# Test that a doctest is correctly identified and formatted with a blank -# continuation line. -def doctest_blank_continued(): - """ - Do cool stuff. +# Extra blanks should be preserved. +def rst_literal_extra_blanks(): + """ + 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. + cool_stuff( 1 ) - >>> 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. + Done. + """ + pass - >>> def cool_stuff ( x ): - ... print( x ) - ... print( x ) - ... - And say something else. - """ - pass +# If a literal block is never properly ended (via a non-empty unindented line), +# then the end of the block should be the last non-empty line. And subsequent +# empty lines should be preserved as-is. +def rst_literal_extra_blanks_at_end(): + """ + Do cool stuff:: -# 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. + cool_stuff( 1 ) - >>> 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. + """ + pass - >>> x = f'''tricksy''' - """ - pass +# A literal block can contain many empty lines and it should not end the block +# if it continues. +def rst_literal_extra_blanks_in_snippet(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + + cool_stuff( 2 ) + + Done. + """ + pass + + +# This tests that a unindented line appearing after an indented line (but where +# the indent is still beyond the minimum) gets formatted properly. +def rst_literal_subsequent_line_not_indented(): + """ + Do cool stuff:: + + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ + pass + + +# This checks that if the first line in a code snippet has been indented with +# tabs, then so long as its "indentation length" is considered bigger than the +# line with `::`, it is reformatted as code. +# +# (If your tabwidth is set to 4, then it looks like the code snippet +# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST +# itself also seems to recognize this as a code block, although it appears +# under-specified.) +def rst_literal_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Another test with tabs, except in this case, if your tabwidth is less than +# 8, than the code snippet actually looks like its indent is *less* than the +# opening line with a `::`. One might presume this means that the code snippet +# is not treated as a literal block and thus not reformatted, but since we +# assume all tabs have tabwidth=8 when computing indentation length, the code +# snippet is actually seen as being more indented than the opening `::` line. +# As with the above example, reST seems to behave the same way here. +def rst_literal_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that if two lines in a literal block are indented to the same level +# but by different means (tabs versus spaces), then we correctly recognize the +# block and format it. +def rst_literal_first_line_tab_second_line_spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that when two lines in a code snippet have weird and inconsistent +# indentation, the code still gets formatted so long as the indent is greater +# than the indent of the `::` line. +# +# In this case, the minimum indent is 5 spaces (from the second line) where as +# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). +# The minimum indent is stripped from each code line. Since tabs aren't +# divisible, the entire tab is stripped, which means the first and second lines +# wind up with the same level of indentation. +# +# An alternative behavior here would be that the tab is replaced with 3 spaces +# instead of being stripped entirely. The code snippet itself would then have +# inconsistent indentation to the point of being invalid Python, and thus code +# formatting would be skipped. +# +# I decided on the former behavior because it seems a bit easier to implement, +# but we might want to switch to the alternative if cases like this show up in +# the real world. ---AG +def rst_literal_odd_indentation(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that having a line with a lone `::` works as an introduction of a +# literal block. +def rst_literal_lone_colon(): + """ + Do cool stuff. + + :: + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_simple(): + """ + .. code-block:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_case_insensitive(): + """ + .. cOdE-bLoCk:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_sourcecode(): + """ + .. sourcecode:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_options(): + """ + .. code-block:: python + :linenos: + :emphasize-lines: 2,3 + :name: blah blah + + cool_stuff( 1 ) + cool_stuff( 2 ) + cool_stuff( 3 ) + cool_stuff( 4 ) + + Done. + """ + pass + + +# In this case, since `pycon` isn't recognized as a Python code snippet, the +# docstring reformatter ignores it. But it then picks up the doctest and +# reformats it. +def rst_directive_doctest(): + """ + .. code-block:: pycon + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +# This checks that if the first non-empty line after the start of a literal +# block is not indented more than the line containing the `::`, then it is not +# treated as a code snippet. +def rst_literal_skipped_first_line_not_indented(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but inserts an indented line after the un-indented one. +# This should not cause the literal block to be resumed. +def rst_literal_skipped_first_line_not_indented_then_indented(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# This also checks that a code snippet is not reformatted when the indentation +# of the first line is not more than the line with `::`, but this uses tabs to +# make it a little more confounding. It relies on the fact that indentation +# length is computed by assuming a tabwidth equal to 8. reST also rejects this +# and doesn't treat it as a literal block. +def rst_literal_skipped_first_line_not_indented_tab(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the previous test, but adds a second line. +def rst_literal_skipped_first_line_not_indented_tab_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that a code block with a second line that is not properly indented gets +# skipped. A valid code block needs to have an empty line separating these. +# +# One trick here is that we need to make sure the Python code in the snippet is +# valid, otherwise it would be skipped because of invalid Python. +def rst_literal_skipped_subsequent_line_not_indented(): + """ + Do cool stuff:: + + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ + pass + + +# In this test, we write what looks like a code-block, but it should be treated +# as invalid due to the missing `language` argument. +# +# It does still look like it could be a literal block according to the literal +# rules, but we currently consider the `.. ` prefix to indicate that it is not +# a literal block. +def rst_literal_skipped_not_directive(): + """ + .. code-block:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# In this test, we start a line with `.. `, which makes it look like it might +# be a directive. But instead continue it as if it was just some periods from +# the previous line, and then try to end it by starting a literal block. +# +# But because of the `.. ` in the beginning, we wind up not treating this as a +# code snippet. The reST render I was using to test things does actually treat +# this as a code block, so we may be out of conformance here. +def rst_literal_skipped_possible_false_negative(): + """ + This is a test. + .. This is a test:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# This tests that a doctest inside of a reST literal block doesn't get +# reformatted. It's plausible this isn't the right behavior, but it also seems +# like it might be the right behavior since it is a literal block. (The doctest +# makes the Python code invalid.) +def rst_literal_skipped_doctest(): + """ + Do cool stuff:: + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_literal_skipped_markdown(): + """ + Do cool stuff:: + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_skipped_wrong_language(): + """ + .. code-block:: rust + + cool_stuff( 1 ) + + Done. + """ + pass + + +# This gets skipped for the same reason that the doctest in a literal block +# gets skipped. +def rst_directive_skipped_doctest(): + """ + .. code-block:: python + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +############################################################################### +# Markdown CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Markdown fenced code blocks. +# +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks +############################################################################### + + +def markdown_simple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_simple_continued(): + """ + Do cool stuff. + + ```python + def cool_stuff( x ): + print( f"hi {x}" ); + ``` + + Done. + """ + pass + + +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): + """ + Do cool stuff. + + ``` + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): + """ + Do cool stuff. + + ~~~py + cool_stuff( 1 ) + ~~~ + + Done. + """ + pass + + +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + `````` + + Done. + """ + pass + + +# Tests that an invalid closing fence is treated as invalid. +# +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ''' + ```invalid + ''' + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): + """ + Do cool stuff. + + `````` + do_something( ''' + ``` + did i trick you? + ``` + ''' ) + `````` + + Done. + """ + pass + + +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + + + + """ + pass + + +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + """ + pass + + +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + """ + pass + + +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ```""" + pass + + +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 )""" + pass + + +def markdown_with_blank_lines(): + """ + Do cool stuff. + + ```py + def cool_stuff( x ): + print( f"hi {x}" ); + + def other_stuff( y ): + print( y ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_tab_second_line_spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_odd_indentation(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): + """ + Do cool stuff. + + ```py + + + cool_stuff( 1 ) + + + ``` + + Done. + """ + pass + + +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): + """ + Do cool stuff. + + ```py + + cool_stuff( 1 ) + + + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_weird_closing(): + """ + Code block with weirdly placed closing fences. + + ```python + cool_stuff( 1 ) + + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` + + Now the code block is closed + """ + pass + + +def markdown_over_indented(): + """ + A docstring + over intended + ```python + print( 5 ) + ``` + """ + pass + + +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. + """ + pass + + +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` + + Done. + """ + pass + + +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. +# +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. +# +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_skipped_doctest(): + """ + Do cool stuff. + + ```py + >>> cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_skipped_rst_literal(): + """ + Do cool stuff. + + ```py + And do this:: + + cool_stuff( 1 ) + + ``` + + Done. + """ + pass + + +def markdown_skipped_rst_directive(): + """ + Do cool stuff. + + ```py + .. code-block:: python + + cool_stuff( 1 ) + + ``` + + Done. + """ + 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 +############################################################################### +# DOCTEST CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Python's "doctest" format. +# +# See: https://docs.python.org/3/library/doctest.html +############################################################################### + +# 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 on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line(): + """ + Do cool stuff. + + >>> cool_stuff( x )""" + pass + + +# Test that a continued doctest on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line_continued(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + ... more( y )""" + 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 + + +############################################################################### +# reStructuredText CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# reStructuredText formatted code blocks. +# +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 +############################################################################### + + +def rst_literal_simple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_literal_simple_continued(): + """ + Do cool stuff:: + + def cool_stuff( x ): + print( f"hi {x}" ); + + Done. + """ + pass + + +# Tests that we can end the literal block on the second +# to last line of the docstring. +def rst_literal_second_to_last(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + """ + pass + + +# Tests that we can end the literal block on the actual +# last line of the docstring. +def rst_literal_actually_last(): + """ + Do cool stuff:: + + cool_stuff( 1 )""" + pass + + +def rst_literal_with_blank_lines(): + """ + Do cool stuff:: + + def cool_stuff( x ): + print( f"hi {x}" ); + + def other_stuff( y ): + print( y ) + + Done. + """ + pass + + +# Extra blanks should be preserved. +def rst_literal_extra_blanks(): + """ + Do cool stuff:: + + + + cool_stuff( 1 ) + + + + Done. + """ + pass + + +# If a literal block is never properly ended (via a non-empty unindented line), +# then the end of the block should be the last non-empty line. And subsequent +# empty lines should be preserved as-is. +def rst_literal_extra_blanks_at_end(): + """ + Do cool stuff:: + + + cool_stuff( 1 ) + + + + """ + pass + + +# A literal block can contain many empty lines and it should not end the block +# if it continues. +def rst_literal_extra_blanks_in_snippet(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + + cool_stuff( 2 ) + + Done. + """ + pass + + +# This tests that a unindented line appearing after an indented line (but where +# the indent is still beyond the minimum) gets formatted properly. +def rst_literal_subsequent_line_not_indented(): + """ + Do cool stuff:: + + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ + pass + + +# This checks that if the first line in a code snippet has been indented with +# tabs, then so long as its "indentation length" is considered bigger than the +# line with `::`, it is reformatted as code. +# +# (If your tabwidth is set to 4, then it looks like the code snippet +# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST +# itself also seems to recognize this as a code block, although it appears +# under-specified.) +def rst_literal_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Another test with tabs, except in this case, if your tabwidth is less than +# 8, than the code snippet actually looks like its indent is *less* than the +# opening line with a `::`. One might presume this means that the code snippet +# is not treated as a literal block and thus not reformatted, but since we +# assume all tabs have tabwidth=8 when computing indentation length, the code +# snippet is actually seen as being more indented than the opening `::` line. +# As with the above example, reST seems to behave the same way here. +def rst_literal_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that if two lines in a literal block are indented to the same level +# but by different means (tabs versus spaces), then we correctly recognize the +# block and format it. +def rst_literal_first_line_tab_second_line_spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that when two lines in a code snippet have weird and inconsistent +# indentation, the code still gets formatted so long as the indent is greater +# than the indent of the `::` line. +# +# In this case, the minimum indent is 5 spaces (from the second line) where as +# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). +# The minimum indent is stripped from each code line. Since tabs aren't +# divisible, the entire tab is stripped, which means the first and second lines +# wind up with the same level of indentation. +# +# An alternative behavior here would be that the tab is replaced with 3 spaces +# instead of being stripped entirely. The code snippet itself would then have +# inconsistent indentation to the point of being invalid Python, and thus code +# formatting would be skipped. +# +# I decided on the former behavior because it seems a bit easier to implement, +# but we might want to switch to the alternative if cases like this show up in +# the real world. ---AG +def rst_literal_odd_indentation(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that having a line with a lone `::` works as an introduction of a +# literal block. +def rst_literal_lone_colon(): + """ + Do cool stuff. + + :: + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_simple(): + """ + .. code-block:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_case_insensitive(): + """ + .. cOdE-bLoCk:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_sourcecode(): + """ + .. sourcecode:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_options(): + """ + .. code-block:: python + :linenos: + :emphasize-lines: 2,3 + :name: blah blah + + cool_stuff( 1 ) + cool_stuff( 2 ) + cool_stuff( 3 ) + cool_stuff( 4 ) + + Done. + """ + pass + + +# In this case, since `pycon` isn't recognized as a Python code snippet, the +# docstring reformatter ignores it. But it then picks up the doctest and +# reformats it. +def rst_directive_doctest(): + """ + .. code-block:: pycon + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +# This checks that if the first non-empty line after the start of a literal +# block is not indented more than the line containing the `::`, then it is not +# treated as a code snippet. +def rst_literal_skipped_first_line_not_indented(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but inserts an indented line after the un-indented one. +# This should not cause the literal block to be resumed. +def rst_literal_skipped_first_line_not_indented_then_indented(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# This also checks that a code snippet is not reformatted when the indentation +# of the first line is not more than the line with `::`, but this uses tabs to +# make it a little more confounding. It relies on the fact that indentation +# length is computed by assuming a tabwidth equal to 8. reST also rejects this +# and doesn't treat it as a literal block. +def rst_literal_skipped_first_line_not_indented_tab(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the previous test, but adds a second line. +def rst_literal_skipped_first_line_not_indented_tab_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that a code block with a second line that is not properly indented gets +# skipped. A valid code block needs to have an empty line separating these. +# +# One trick here is that we need to make sure the Python code in the snippet is +# valid, otherwise it would be skipped because of invalid Python. +def rst_literal_skipped_subsequent_line_not_indented(): + """ + Do cool stuff:: + + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ + pass + + +# In this test, we write what looks like a code-block, but it should be treated +# as invalid due to the missing `language` argument. +# +# It does still look like it could be a literal block according to the literal +# rules, but we currently consider the `.. ` prefix to indicate that it is not +# a literal block. +def rst_literal_skipped_not_directive(): + """ + .. code-block:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# In this test, we start a line with `.. `, which makes it look like it might +# be a directive. But instead continue it as if it was just some periods from +# the previous line, and then try to end it by starting a literal block. +# +# But because of the `.. ` in the beginning, we wind up not treating this as a +# code snippet. The reST render I was using to test things does actually treat +# this as a code block, so we may be out of conformance here. +def rst_literal_skipped_possible_false_negative(): + """ + This is a test. + .. This is a test:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# This tests that a doctest inside of a reST literal block doesn't get +# reformatted. It's plausible this isn't the right behavior, but it also seems +# like it might be the right behavior since it is a literal block. (The doctest +# makes the Python code invalid.) +def rst_literal_skipped_doctest(): + """ + Do cool stuff:: + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_literal_skipped_markdown(): + """ + Do cool stuff:: + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_skipped_wrong_language(): + """ + .. code-block:: rust + + cool_stuff( 1 ) + + Done. + """ + pass + + +# This gets skipped for the same reason that the doctest in a literal block +# gets skipped. +def rst_directive_skipped_doctest(): + """ + .. code-block:: python + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +############################################################################### +# Markdown CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Markdown fenced code blocks. +# +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks +############################################################################### + + +def markdown_simple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_simple_continued(): + """ + Do cool stuff. + + ```python + def cool_stuff( x ): + print( f"hi {x}" ); + ``` + + Done. + """ + pass + + +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): + """ + Do cool stuff. + + ``` + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): + """ + Do cool stuff. + + ~~~py + cool_stuff( 1 ) + ~~~ + + Done. + """ + pass + + +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + `````` + + Done. + """ + pass + + +# Tests that an invalid closing fence is treated as invalid. +# +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ''' + ```invalid + ''' + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): + """ + Do cool stuff. + + `````` + do_something( ''' + ``` + did i trick you? + ``` + ''' ) + `````` + + Done. + """ + pass + + +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + + + + """ + pass + + +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + """ + pass + + +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + """ + pass + + +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ```""" + pass + + +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 )""" + pass + + +def markdown_with_blank_lines(): + """ + Do cool stuff. + + ```py + def cool_stuff( x ): + print( f"hi {x}" ); + + def other_stuff( y ): + print( y ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_tab_second_line_spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_odd_indentation(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): + """ + Do cool stuff. + + ```py + + + cool_stuff( 1 ) + + + ``` + + Done. + """ + pass + + +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): + """ + Do cool stuff. + + ```py + + cool_stuff( 1 ) + + + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_weird_closing(): + """ + Code block with weirdly placed closing fences. + + ```python + cool_stuff( 1 ) + + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` + + Now the code block is closed + """ + pass + + +def markdown_over_indented(): + """ + A docstring + over intended + ```python + print( 5 ) + ``` + """ + pass + + +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. + """ + pass + + +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` + + Done. + """ + pass + + +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. +# +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. +# +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_skipped_doctest(): + """ + Do cool stuff. + + ```py + >>> cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_skipped_rst_literal(): + """ + Do cool stuff. + + ```py + And do this:: + + cool_stuff( 1 ) + + ``` + + Done. + """ + pass + + +def markdown_skipped_rst_directive(): + """ + Do cool stuff. + + ```py + .. code-block:: python + + cool_stuff( 1 ) + + ``` + + Done. + """ + 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 +############################################################################### +# DOCTEST CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Python's "doctest" format. +# +# See: https://docs.python.org/3/library/doctest.html +############################################################################### + +# 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 on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line(): + """ + Do cool stuff. + + >>> cool_stuff( x )""" + pass + + +# Test that a continued doctest on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line_continued(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + ... more( y )""" + 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 + + +############################################################################### +# reStructuredText CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# reStructuredText formatted code blocks. +# +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 +############################################################################### + + +def rst_literal_simple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_literal_simple_continued(): + """ + Do cool stuff:: + + def cool_stuff( x ): + print( f"hi {x}" ); + + Done. + """ + pass + + +# Tests that we can end the literal block on the second +# to last line of the docstring. +def rst_literal_second_to_last(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + """ + pass + + +# Tests that we can end the literal block on the actual +# last line of the docstring. +def rst_literal_actually_last(): + """ + Do cool stuff:: + + cool_stuff( 1 )""" + pass + + +def rst_literal_with_blank_lines(): + """ + Do cool stuff:: + + def cool_stuff( x ): + print( f"hi {x}" ); + + def other_stuff( y ): + print( y ) + + Done. + """ + pass + + +# Extra blanks should be preserved. +def rst_literal_extra_blanks(): + """ + Do cool stuff:: + + + + cool_stuff( 1 ) + + + + Done. + """ + pass + + +# If a literal block is never properly ended (via a non-empty unindented line), +# then the end of the block should be the last non-empty line. And subsequent +# empty lines should be preserved as-is. +def rst_literal_extra_blanks_at_end(): + """ + Do cool stuff:: + + + cool_stuff( 1 ) + + + + """ + pass + + +# A literal block can contain many empty lines and it should not end the block +# if it continues. +def rst_literal_extra_blanks_in_snippet(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + + cool_stuff( 2 ) + + Done. + """ + pass + + +# This tests that a unindented line appearing after an indented line (but where +# the indent is still beyond the minimum) gets formatted properly. +def rst_literal_subsequent_line_not_indented(): + """ + Do cool stuff:: + + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ + pass + + +# This checks that if the first line in a code snippet has been indented with +# tabs, then so long as its "indentation length" is considered bigger than the +# line with `::`, it is reformatted as code. +# +# (If your tabwidth is set to 4, then it looks like the code snippet +# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST +# itself also seems to recognize this as a code block, although it appears +# under-specified.) +def rst_literal_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Another test with tabs, except in this case, if your tabwidth is less than +# 8, than the code snippet actually looks like its indent is *less* than the +# opening line with a `::`. One might presume this means that the code snippet +# is not treated as a literal block and thus not reformatted, but since we +# assume all tabs have tabwidth=8 when computing indentation length, the code +# snippet is actually seen as being more indented than the opening `::` line. +# As with the above example, reST seems to behave the same way here. +def rst_literal_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that if two lines in a literal block are indented to the same level +# but by different means (tabs versus spaces), then we correctly recognize the +# block and format it. +def rst_literal_first_line_tab_second_line_spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that when two lines in a code snippet have weird and inconsistent +# indentation, the code still gets formatted so long as the indent is greater +# than the indent of the `::` line. +# +# In this case, the minimum indent is 5 spaces (from the second line) where as +# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). +# The minimum indent is stripped from each code line. Since tabs aren't +# divisible, the entire tab is stripped, which means the first and second lines +# wind up with the same level of indentation. +# +# An alternative behavior here would be that the tab is replaced with 3 spaces +# instead of being stripped entirely. The code snippet itself would then have +# inconsistent indentation to the point of being invalid Python, and thus code +# formatting would be skipped. +# +# I decided on the former behavior because it seems a bit easier to implement, +# but we might want to switch to the alternative if cases like this show up in +# the real world. ---AG +def rst_literal_odd_indentation(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that having a line with a lone `::` works as an introduction of a +# literal block. +def rst_literal_lone_colon(): + """ + Do cool stuff. + + :: + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_simple(): + """ + .. code-block:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_case_insensitive(): + """ + .. cOdE-bLoCk:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_sourcecode(): + """ + .. sourcecode:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_options(): + """ + .. code-block:: python + :linenos: + :emphasize-lines: 2,3 + :name: blah blah + + cool_stuff( 1 ) + cool_stuff( 2 ) + cool_stuff( 3 ) + cool_stuff( 4 ) + + Done. + """ + pass + + +# In this case, since `pycon` isn't recognized as a Python code snippet, the +# docstring reformatter ignores it. But it then picks up the doctest and +# reformats it. +def rst_directive_doctest(): + """ + .. code-block:: pycon + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +# This checks that if the first non-empty line after the start of a literal +# block is not indented more than the line containing the `::`, then it is not +# treated as a code snippet. +def rst_literal_skipped_first_line_not_indented(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but inserts an indented line after the un-indented one. +# This should not cause the literal block to be resumed. +def rst_literal_skipped_first_line_not_indented_then_indented(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# This also checks that a code snippet is not reformatted when the indentation +# of the first line is not more than the line with `::`, but this uses tabs to +# make it a little more confounding. It relies on the fact that indentation +# length is computed by assuming a tabwidth equal to 8. reST also rejects this +# and doesn't treat it as a literal block. +def rst_literal_skipped_first_line_not_indented_tab(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the previous test, but adds a second line. +def rst_literal_skipped_first_line_not_indented_tab_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that a code block with a second line that is not properly indented gets +# skipped. A valid code block needs to have an empty line separating these. +# +# One trick here is that we need to make sure the Python code in the snippet is +# valid, otherwise it would be skipped because of invalid Python. +def rst_literal_skipped_subsequent_line_not_indented(): + """ + Do cool stuff:: + + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ + pass + + +# In this test, we write what looks like a code-block, but it should be treated +# as invalid due to the missing `language` argument. +# +# It does still look like it could be a literal block according to the literal +# rules, but we currently consider the `.. ` prefix to indicate that it is not +# a literal block. +def rst_literal_skipped_not_directive(): + """ + .. code-block:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# In this test, we start a line with `.. `, which makes it look like it might +# be a directive. But instead continue it as if it was just some periods from +# the previous line, and then try to end it by starting a literal block. +# +# But because of the `.. ` in the beginning, we wind up not treating this as a +# code snippet. The reST render I was using to test things does actually treat +# this as a code block, so we may be out of conformance here. +def rst_literal_skipped_possible_false_negative(): + """ + This is a test. + .. This is a test:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# This tests that a doctest inside of a reST literal block doesn't get +# reformatted. It's plausible this isn't the right behavior, but it also seems +# like it might be the right behavior since it is a literal block. (The doctest +# makes the Python code invalid.) +def rst_literal_skipped_doctest(): + """ + Do cool stuff:: + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_literal_skipped_markdown(): + """ + Do cool stuff:: + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_skipped_wrong_language(): + """ + .. code-block:: rust + + cool_stuff( 1 ) + + Done. + """ + pass + + +# This gets skipped for the same reason that the doctest in a literal block +# gets skipped. +def rst_directive_skipped_doctest(): + """ + .. code-block:: python + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +############################################################################### +# Markdown CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Markdown fenced code blocks. +# +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks +############################################################################### + + +def markdown_simple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_simple_continued(): + """ + Do cool stuff. + + ```python + def cool_stuff( x ): + print( f"hi {x}" ); + ``` + + Done. + """ + pass + + +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): + """ + Do cool stuff. + + ``` + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): + """ + Do cool stuff. + + ~~~py + cool_stuff( 1 ) + ~~~ + + Done. + """ + pass + + +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + `````` + + Done. + """ + pass + + +# Tests that an invalid closing fence is treated as invalid. +# +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ''' + ```invalid + ''' + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): + """ + Do cool stuff. + + `````` + do_something( ''' + ``` + did i trick you? + ``` + ''' ) + `````` + + Done. + """ + pass + + +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + + + + """ + pass + + +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + """ + pass + + +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + """ + pass + + +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ```""" + pass + + +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 )""" + pass + + +def markdown_with_blank_lines(): + """ + Do cool stuff. + + ```py + def cool_stuff( x ): + print( f"hi {x}" ); + + def other_stuff( y ): + print( y ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_first_line_tab_second_line_spaces(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_odd_indentation(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): + """ + Do cool stuff. + + ```py + + + cool_stuff( 1 ) + + + ``` + + Done. + """ + pass + + +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): + """ + Do cool stuff. + + ```py + + cool_stuff( 1 ) + + + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_weird_closing(): + """ + Code block with weirdly placed closing fences. + + ```python + cool_stuff( 1 ) + + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` + + Now the code block is closed + """ + pass + + +def markdown_over_indented(): + """ + A docstring + over intended + ```python + print( 5 ) + ``` + """ + pass + + +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. + """ + pass + + +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` + + Done. + """ + pass + + +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. +# +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. +# +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. + """ + pass + + +def markdown_skipped_doctest(): + """ + Do cool stuff. + + ```py + >>> cool_stuff( 1 ) + ``` + + Done. + """ + pass + + +def markdown_skipped_rst_literal(): + """ + Do cool stuff. + + ```py + And do this:: + + cool_stuff( 1 ) + + ``` + + Done. + """ + pass + + +def markdown_skipped_rst_directive(): + """ + Do cool stuff. + + ```py + .. code-block:: python + + cool_stuff( 1 ) + + ``` + + Done. + """ + 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 +############################################################################### +# DOCTEST CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Python's "doctest" format. +# +# See: https://docs.python.org/3/library/doctest.html +############################################################################### + +# 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 on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line(): + """ + Do cool stuff. + + >>> cool_stuff( x )""" + pass + + +# Test that a continued doctest on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line_continued(): + """ + Do cool stuff. + + >>> cool_stuff( x ) + ... more( y )""" + 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 + + +############################################################################### +# reStructuredText CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# reStructuredText formatted code blocks. +# +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 +############################################################################### + + +def rst_literal_simple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_literal_simple_continued(): + """ + Do cool stuff:: + + def cool_stuff( x ): + print( f"hi {x}" ); + + Done. + """ + pass + + +# Tests that we can end the literal block on the second +# to last line of the docstring. +def rst_literal_second_to_last(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + """ + pass + + +# Tests that we can end the literal block on the actual +# last line of the docstring. +def rst_literal_actually_last(): + """ + Do cool stuff:: + + cool_stuff( 1 )""" + pass + + +def rst_literal_with_blank_lines(): + """ + Do cool stuff:: + + def cool_stuff( x ): + print( f"hi {x}" ); + + def other_stuff( y ): + print( y ) + + Done. + """ + pass + + +# Extra blanks should be preserved. +def rst_literal_extra_blanks(): + """ + Do cool stuff:: + + + + cool_stuff( 1 ) + + + + Done. + """ + pass + + +# If a literal block is never properly ended (via a non-empty unindented line), +# then the end of the block should be the last non-empty line. And subsequent +# empty lines should be preserved as-is. +def rst_literal_extra_blanks_at_end(): + """ + Do cool stuff:: + + + cool_stuff( 1 ) + + + + """ + pass + + +# A literal block can contain many empty lines and it should not end the block +# if it continues. +def rst_literal_extra_blanks_in_snippet(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + + cool_stuff( 2 ) + + Done. + """ + pass + + +# This tests that a unindented line appearing after an indented line (but where +# the indent is still beyond the minimum) gets formatted properly. +def rst_literal_subsequent_line_not_indented(): + """ + Do cool stuff:: + + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ + pass + + +# This checks that if the first line in a code snippet has been indented with +# tabs, then so long as its "indentation length" is considered bigger than the +# line with `::`, it is reformatted as code. +# +# (If your tabwidth is set to 4, then it looks like the code snippet +# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST +# itself also seems to recognize this as a code block, although it appears +# under-specified.) +def rst_literal_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Another test with tabs, except in this case, if your tabwidth is less than +# 8, than the code snippet actually looks like its indent is *less* than the +# opening line with a `::`. One might presume this means that the code snippet +# is not treated as a literal block and thus not reformatted, but since we +# assume all tabs have tabwidth=8 when computing indentation length, the code +# snippet is actually seen as being more indented than the opening `::` line. +# As with the above example, reST seems to behave the same way here. +def rst_literal_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff:: + + cool_stuff( 1 ) -# 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. + Done. + """ + pass - >>> x = '''\"\"\"''' - """ - pass +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff:: -# Tests that inverting the triple quoting works as expected. -def doctest_with_triple_inverted(): - ''' - Do cool stuff. + cool_stuff( 1 ) + cool_stuff( 2 ) - >>> x = """tricksy""" - ''' - pass + Done. + """ + pass -# Tests that inverting the triple quoting with an f-string works as -# expected. -def doctest_with_triple_inverted_fstring(): - ''' - Do cool stuff. +# Tests that if two lines in a literal block are indented to the same level +# but by different means (tabs versus spaces), then we correctly recognize the +# block and format it. +def rst_literal_first_line_tab_second_line_spaces(): + """ + Do cool stuff:: - >>> x = f"""tricksy""" - ''' - pass + cool_stuff( 1 ) + cool_stuff( 2 ) + Done. + """ + pass -# Tests nested doctests are ignored. That is, we don't format doctests -# recursively. We only recognize "top level" doctests. + +# Tests that when two lines in a code snippet have weird and inconsistent +# indentation, the code still gets formatted so long as the indent is greater +# than the indent of the `::` line. # -# 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. +# In this case, the minimum indent is 5 spaces (from the second line) where as +# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). +# The minimum indent is stripped from each code line. Since tabs aren't +# divisible, the entire tab is stripped, which means the first and second lines +# wind up with the same level of indentation. +# +# An alternative behavior here would be that the tab is replaced with 3 spaces +# instead of being stripped entirely. The code snippet itself would then have +# inconsistent indentation to the point of being invalid Python, and thus code +# formatting would be skipped. +# +# I decided on the former behavior because it seems a bit easier to implement, +# but we might want to switch to the alternative if cases like this show up in +# the real world. ---AG +def rst_literal_odd_indentation(): + """ + Do cool stuff:: - >>> def nested( x ): - ... """ - ... Do nested cool stuff. - ... >>> func_call( 5 ) - ... """ - ... pass - ''' - pass + cool_stuff( 1 ) + cool_stuff( 2 ) + Done. + """ + 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 having a line with a lone `::` works as an introduction of a +# literal block. +def rst_literal_lone_colon(): + """ + Do cool stuff. + :: -# Tests that long lines get wrapped... appropriately. + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_simple(): + """ + .. code-block:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_case_insensitive(): + """ + .. cOdE-bLoCk:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_sourcecode(): + """ + .. sourcecode:: python + + cool_stuff( 1 ) + + Done. + """ + pass + + +def rst_directive_options(): + """ + .. code-block:: python + :linenos: + :emphasize-lines: 2,3 + :name: blah blah + + cool_stuff( 1 ) + cool_stuff( 2 ) + cool_stuff( 3 ) + cool_stuff( 4 ) + + Done. + """ + pass + + +# In this case, since `pycon` isn't recognized as a Python code snippet, the +# docstring reformatter ignores it. But it then picks up the doctest and +# reformats it. +def rst_directive_doctest(): + """ + .. code-block:: pycon + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +# This checks that if the first non-empty line after the start of a literal +# block is not indented more than the line containing the `::`, then it is not +# treated as a code snippet. +def rst_literal_skipped_first_line_not_indented(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the test above, but inserts an indented line after the un-indented one. +# This should not cause the literal block to be resumed. +def rst_literal_skipped_first_line_not_indented_then_indented(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# This also checks that a code snippet is not reformatted when the indentation +# of the first line is not more than the line with `::`, but this uses tabs to +# make it a little more confounding. It relies on the fact that indentation +# length is computed by assuming a tabwidth equal to 8. reST also rejects this +# and doesn't treat it as a literal block. +def rst_literal_skipped_first_line_not_indented_tab(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + + Done. + """ + pass + + +# Like the previous test, but adds a second line. +def rst_literal_skipped_first_line_not_indented_tab_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ + pass + + +# Tests that a code block with a second line that is not properly indented gets +# skipped. A valid code block needs to have an empty line separating these. # -# 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. +# One trick here is that we need to make sure the Python code in the snippet is +# valid, otherwise it would be skipped because of invalid Python. +def rst_literal_skipped_subsequent_line_not_indented(): + """ + Do cool stuff:: + + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ + pass + + +# In this test, we write what looks like a code-block, but it should be treated +# as invalid due to the missing `language` argument. # -# 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. +# It does still look like it could be a literal block according to the literal +# rules, but we currently consider the `.. ` prefix to indicate that it is not +# a literal block. +def rst_literal_skipped_not_directive(): + """ + .. code-block:: - 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) + cool_stuff( 1 ) - 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 - ) + Done. + """ + pass -# Checks that a simple but invalid doctest gets skipped. -def doctest_skipped_simple(): - """ - Do cool stuff. +# In this test, we start a line with `.. `, which makes it look like it might +# be a directive. But instead continue it as if it was just some periods from +# the previous line, and then try to end it by starting a literal block. +# +# But because of the `.. ` in the beginning, we wind up not treating this as a +# code snippet. The reST render I was using to test things does actually treat +# this as a code block, so we may be out of conformance here. +def rst_literal_skipped_possible_false_negative(): + """ + This is a test. + .. This is a test:: - >>> cool-stuff( x ): - 2 - """ - pass + cool_stuff( 1 ) + Done. + """ + 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 +# This tests that a doctest inside of a reST literal block doesn't get +# reformatted. It's plausible this isn't the right behavior, but it also seems +# like it might be the right behavior since it is a literal block. (The doctest +# makes the Python code invalid.) +def rst_literal_skipped_doctest(): + """ + Do cool stuff:: + >>> cool_stuff( 1 ) -# Checks that a doctest with improper indentation gets skipped. -def doctest_skipped_inconsistent_indent(): - """ - Do cool stuff. + Done. + """ + pass - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 - """ - pass +def rst_literal_skipped_markdown(): + """ + Do cool stuff:: -# 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. + ```py + cool_stuff( 1 ) + ``` - >>> def cool_stuff( x ): - ... print( x ) - ... print( f"hi {x}" ); - hi 2 - """ - pass + Done. + """ + 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. +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python - >>> foo( x ) - ... '''tri'''cksy''' - """ - pass + cool_stuff( 1 ) + + Done. + """ + pass -# Tests that a doctest on a single line is skipped. -def doctest_skipped_one_line(): - ">>> foo( x )" - pass +def rst_directive_skipped_wrong_language(): + """ + .. code-block:: rust + cool_stuff( 1 ) -# 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. + Done. + """ + pass - >>> cool_stuff( 1 ) - 2 - """ - pass +# This gets skipped for the same reason that the doctest in a literal block +# gets skipped. +def rst_directive_skipped_doctest(): + """ + .. code-block:: python -# 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. + >>> cool_stuff( 1 ) - >>> x = '\"\"\"' - """ - pass + Done. + """ + pass ############################################################################### -# reStructuredText CODE EXAMPLES +# Markdown CODE EXAMPLES # # This section shows examples of docstrings that contain code snippets in -# reStructuredText formatted code blocks. +# Markdown fenced code blocks. # -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks ############################################################################### -def rst_literal_simple(): - """ - Do cool stuff:: - - cool_stuff( 1 ) - - Done. - """ - pass - - -def rst_literal_simple_continued(): - """ - Do cool stuff:: +def markdown_simple(): + """ + Do cool stuff. - def cool_stuff( x ): - print( f"hi {x}" ); + ```py + cool_stuff( 1 ) + ``` - Done. - """ - pass + Done. + """ + pass -# Tests that we can end the literal block on the second -# to last line of the docstring. -def rst_literal_second_to_last(): - """ - Do cool stuff:: +def markdown_simple_continued(): + """ + Do cool stuff. - cool_stuff( 1 ) - """ - pass + ```python + def cool_stuff( x ): + print( f"hi {x}" ); + ``` + Done. + """ + pass -# Tests that we can end the literal block on the actual -# last line of the docstring. -def rst_literal_actually_last(): - """ - Do cool stuff:: - cool_stuff( 1 )""" - pass +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): + """ + Do cool stuff. + ``` + cool_stuff( 1 ) + ``` -def rst_literal_with_blank_lines(): - """ - Do cool stuff:: + Done. + """ + pass - def cool_stuff( x ): - print( f"hi {x}" ); - def other_stuff( y ): - print( y ) +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): + """ + Do cool stuff. - Done. - """ - pass + ~~~py + cool_stuff( 1 ) + ~~~ + Done. + """ + pass -# Extra blanks should be preserved. -def rst_literal_extra_blanks(): - """ - Do cool stuff:: +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + ```py + cool_stuff( 1 ) + `````` - cool_stuff( 1 ) + Done. + """ + pass +# Tests that an invalid closing fence is treated as invalid. +# +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff( 1 ) + ''' + ```invalid + ''' + cool_stuff( 2 ) + ``` + Done. + """ + pass -# If a literal block is never properly ended (via a non-empty unindented line), -# then the end of the block should be the last non-empty line. And subsequent -# empty lines should be preserved as-is. -def rst_literal_extra_blanks_at_end(): - """ - Do cool stuff:: +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): + """ + Do cool stuff. - cool_stuff( 1 ) + `````` + do_something( ''' + ``` + did i trick you? + ``` + ''' ) + `````` + Done. + """ + pass - """ - pass +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): + """ + Do cool stuff. + ```py + cool_stuff( 1 ) -# A literal block can contain many empty lines and it should not end the block -# if it continues. -def rst_literal_extra_blanks_in_snippet(): - """ - Do cool stuff:: - cool_stuff( 1 ) + """ + pass - cool_stuff( 2 ) - Done. - """ - pass +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): + """ + Do cool stuff. + ```py + cool_stuff( 1 ) + ``` + """ + pass -# This tests that a unindented line appearing after an indented line (but where -# the indent is still beyond the minimum) gets formatted properly. -def rst_literal_subsequent_line_not_indented(): - """ - Do cool stuff:: - if True: - cool_stuff( ''' - hiya''' ) +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff( 1 ) + """ + pass -# This checks that if the first line in a code snippet has been indented with -# tabs, then so long as its "indentation length" is considered bigger than the -# line with `::`, it is reformatted as code. -# -# (If your tabwidth is set to 4, then it looks like the code snippet -# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST -# itself also seems to recognize this as a code block, although it appears -# under-specified.) -def rst_literal_first_line_indent_uses_tabs_4spaces(): - """ - Do cool stuff:: +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): + """ + Do cool stuff. - cool_stuff( 1 ) + ```py + cool_stuff( 1 ) + ```""" + pass - Done. - """ - pass +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): + """ + Do cool stuff. -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): - """ - Do cool stuff:: + ```py + cool_stuff( 1 )""" + pass - cool_stuff( 1 ) - cool_stuff( 2 ) - Done. - """ - pass +def markdown_with_blank_lines(): + """ + Do cool stuff. + ```py + def cool_stuff( x ): + print( f"hi {x}" ); -# Another test with tabs, except in this case, if your tabwidth is less than -# 8, than the code snippet actually looks like its indent is *less* than the -# opening line with a `::`. One might presume this means that the code snippet -# is not treated as a literal block and thus not reformatted, but since we -# assume all tabs have tabwidth=8 when computing indentation length, the code -# snippet is actually seen as being more indented than the opening `::` line. -# As with the above example, reST seems to behave the same way here. -def rst_literal_first_line_indent_uses_tabs_8spaces(): - """ - Do cool stuff:: + def other_stuff( y ): + print( y ) + ``` - cool_stuff( 1 ) + Done. + """ + pass - Done. - """ - pass +def markdown_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff. -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): - """ - Do cool stuff:: + ```py + cool_stuff( 1 ) + ``` - cool_stuff( 1 ) - cool_stuff( 2 ) + Done. + """ + pass - Done. - """ - pass +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff. -# Tests that if two lines in a literal block are indented to the same level -# but by different means (tabs versus spaces), then we correctly recognize the -# block and format it. -def rst_literal_first_line_tab_second_line_spaces(): - """ - Do cool stuff:: + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` - cool_stuff( 1 ) - cool_stuff( 2 ) + Done. + """ + pass - Done. - """ - pass +def markdown_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff. -# Tests that when two lines in a code snippet have weird and inconsistent -# indentation, the code still gets formatted so long as the indent is greater -# than the indent of the `::` line. -# -# In this case, the minimum indent is 5 spaces (from the second line) where as -# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). -# The minimum indent is stripped from each code line. Since tabs aren't -# divisible, the entire tab is stripped, which means the first and second lines -# wind up with the same level of indentation. -# -# An alternative behavior here would be that the tab is replaced with 3 spaces -# instead of being stripped entirely. The code snippet itself would then have -# inconsistent indentation to the point of being invalid Python, and thus code -# formatting would be skipped. -# -# I decided on the former behavior because it seems a bit easier to implement, -# but we might want to switch to the alternative if cases like this show up in -# the real world. ---AG -def rst_literal_odd_indentation(): - """ - Do cool stuff:: + ```py + cool_stuff( 1 ) + ``` - cool_stuff( 1 ) - cool_stuff( 2 ) + Done. + """ + pass - Done. - """ - pass +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff. -# Tests that having a line with a lone `::` works as an introduction of a -# literal block. -def rst_literal_lone_colon(): - """ - Do cool stuff. + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` - :: + Done. + """ + pass - cool_stuff( 1 ) - Done. - """ - pass +def markdown_first_line_tab_second_line_spaces(): + """ + Do cool stuff. + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` -def rst_directive_simple(): - """ - .. code-block:: python + Done. + """ + pass - cool_stuff( 1 ) - Done. - """ - pass +def markdown_odd_indentation(): + """ + Do cool stuff. + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` -def rst_directive_case_insensitive(): - """ - .. cOdE-bLoCk:: python + Done. + """ + pass - cool_stuff( 1 ) - Done. - """ - pass +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): + """ + Do cool stuff. + ```py -def rst_directive_sourcecode(): - """ - .. sourcecode:: python - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + ``` -def rst_directive_options(): - """ - .. code-block:: python - :linenos: - :emphasize-lines: 2,3 - :name: blah blah + Done. + """ + pass - cool_stuff( 1 ) - cool_stuff( 2 ) - cool_stuff( 3 ) - cool_stuff( 4 ) - Done. - """ - pass +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): + """ + Do cool stuff. + ```py -# In this case, since `pycon` isn't recognized as a Python code snippet, the -# docstring reformatter ignores it. But it then picks up the doctest and -# reformats it. -def rst_directive_doctest(): - """ - .. code-block:: pycon + cool_stuff( 1 ) - >>> cool_stuff( 1 ) - Done. - """ - pass + cool_stuff( 2 ) + ``` + Done. + """ + pass -# This checks that if the first non-empty line after the start of a literal -# block is not indented more than the line containing the `::`, then it is not -# treated as a code snippet. -def rst_literal_skipped_first_line_not_indented(): - """ - Do cool stuff:: - cool_stuff( 1 ) +def markdown_weird_closing(): + """ + Code block with weirdly placed closing fences. - Done. - """ - pass + ```python + cool_stuff( 1 ) + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` -# Like the test above, but inserts an indented line after the un-indented one. -# This should not cause the literal block to be resumed. -def rst_literal_skipped_first_line_not_indented_then_indented(): - """ - Do cool stuff:: + Now the code block is closed + """ + pass - cool_stuff( 1 ) - cool_stuff( 2 ) - Done. - """ - pass +def markdown_over_indented(): + """ + A docstring + over intended + ```python + print( 5 ) + ``` + """ + pass -# This also checks that a code snippet is not reformatted when the indentation -# of the first line is not more than the line with `::`, but this uses tabs to -# make it a little more confounding. It relies on the fact that indentation -# length is computed by assuming a tabwidth equal to 8. reST also rejects this -# and doesn't treat it as a literal block. -def rst_literal_skipped_first_line_not_indented_tab(): - """ - Do cool stuff:: +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): + """ + Do cool stuff. - cool_stuff( 1 ) + ```py + cool_stuff( 1 ) - Done. - """ - pass + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. + """ + pass -# Like the previous test, but adds a second line. -def rst_literal_skipped_first_line_not_indented_tab_multiple(): - """ - Do cool stuff:: +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): + """ + Do cool stuff. - cool_stuff( 1 ) - cool_stuff( 2 ) + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` - Done. - """ - pass + Done. + """ + pass -# Tests that a code block with a second line that is not properly indented gets -# skipped. A valid code block needs to have an empty line separating these. +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. # -# One trick here is that we need to make sure the Python code in the snippet is -# valid, otherwise it would be skipped because of invalid Python. -def rst_literal_skipped_subsequent_line_not_indented(): - """ - Do cool stuff:: +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): + """ + Do cool stuff. - if True: - cool_stuff( ''' - hiya''' ) + ```py + cool_stuff( 1 ) + ``` - Done. - """ - pass + Done. + """ + pass -# In this test, we write what looks like a code-block, but it should be treated -# as invalid due to the missing `language` argument. -# -# It does still look like it could be a literal block according to the literal -# rules, but we currently consider the `.. ` prefix to indicate that it is not -# a literal block. -def rst_literal_skipped_not_directive(): - """ - .. code-block:: +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): + """ + Do cool stuff. - cool_stuff( 1 ) + ```py + cool_stuff( 1 ) + ``` - Done. - """ - pass + Done. + """ + pass -# In this test, we start a line with `.. `, which makes it look like it might -# be a directive. But instead continue it as if it was just some periods from -# the previous line, and then try to end it by starting a literal block. +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. # -# But because of the `.. ` in the beginning, we wind up not treating this as a -# code snippet. The reST render I was using to test things does actually treat -# this as a code block, so we may be out of conformance here. -def rst_literal_skipped_possible_false_negative(): - """ - This is a test. - .. This is a test:: +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): + """ + Do cool stuff. - cool_stuff( 1 ) + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` - Done. - """ - pass + Done. + """ + pass -# This tests that a doctest inside of a reST literal block doesn't get -# reformatted. It's plausible this isn't the right behavior, but it also seems -# like it might be the right behavior since it is a literal block. (The doctest -# makes the Python code invalid.) -def rst_literal_skipped_doctest(): - """ - Do cool stuff:: +def markdown_skipped_doctest(): + """ + Do cool stuff. - >>> cool_stuff( 1 ) + ```py + >>> cool_stuff( 1 ) + ``` - Done. - """ - pass + Done. + """ + pass -def rst_directive_skipped_not_indented(): - """ - .. code-block:: python +def markdown_skipped_rst_literal(): + """ + Do cool stuff. - cool_stuff( 1 ) + ```py + And do this:: - Done. - """ - pass + cool_stuff( 1 ) + ``` -def rst_directive_skipped_wrong_language(): - """ - .. code-block:: rust + Done. + """ + pass - cool_stuff( 1 ) - Done. - """ - pass +def markdown_skipped_rst_directive(): + """ + Do cool stuff. + ```py + .. code-block:: python -# This gets skipped for the same reason that the doctest in a literal block -# gets skipped. -def rst_directive_skipped_doctest(): - """ - .. code-block:: python + cool_stuff( 1 ) - >>> cool_stuff( 1 ) + ``` - Done. - """ - pass + Done. + """ + pass ``` -### Output 3 +### Output 5 ``` -indent-style = tab +indent-style = space line-width = 88 -indent-width = 8 +indent-width = 4 quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect -docstring-code = Disabled +docstring-code = Enabled preview = Disabled ``` @@ -2559,182 +6787,180 @@ preview = Disabled # The simplest doctest to ensure basic formatting works. def doctest_simple(): - """ - Do cool stuff. + """ + Do cool stuff. - >>> cool_stuff( 1 ) - 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> cool_stuff( x ) - >>> cool_stuff( y ) - 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> cool_stuff( x ) - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - """ - pass + >>> def cool_stuff(x): + ... print(f"hi {x}") + """ + pass # Test that a doctest on the real last line of a docstring reformats # correctly. def doctest_really_last_line(): - """ - Do cool stuff. + """ + Do cool stuff. - >>> cool_stuff( x )""" - pass + >>> cool_stuff(x)""" + pass # Test that a continued doctest on the real last line of a docstring reformats # correctly. def doctest_really_last_line_continued(): - """ - Do cool stuff. - - >>> cool_stuff( x ) - ... more( y )""" - pass + """ + Do cool stuff. + + >>> cool_stuff(x) + ... more(y)""" + pass # Test that a doctest is correctly identified and formatted with a blank # continuation line. def doctest_blank_continued(): - """ - Do cool stuff. + """ + Do cool stuff. - >>> def cool_stuff ( x ): - ... print( x ) - ... - ... print( x ) - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff ( x ): - ... print( x ) - ... print( x ) - ... - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff ( x ): - ... print( x ) - ... print( x ) - ... + >>> def cool_stuff(x): + ... print(x) + ... print(x) - And say something else. - """ - pass + 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. + """ + Do cool stuff. - >>> x = '''tricksy''' - """ - pass + >>> 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. + """ + Do cool stuff. - >>> x = f'''tricksy''' - """ - pass + >>> 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. + """ + Do cool stuff. - >>> x = '''\"\"\"''' - """ - pass + >>> x = '''\"\"\"''' + """ + pass # Tests that inverting the triple quoting works as expected. def doctest_with_triple_inverted(): - ''' - Do cool stuff. + ''' + Do cool stuff. - >>> x = """tricksy""" - ''' - pass + >>> 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. + ''' + Do cool stuff. - >>> x = f"""tricksy""" - ''' - pass + >>> x = f"""tricksy""" + ''' + pass # Tests nested doctests are ignored. That is, we don't format doctests @@ -2744,30 +6970,30 @@ def doctest_with_triple_inverted_fstring(): # 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. + ''' + Do cool stuff. - >>> def nested( x ): - ... """ - ... Do nested cool stuff. - ... >>> func_call( 5 ) - ... """ - ... pass - ''' - pass + >>> 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. + """ + Do cool stuff. - >>> assert ("Easy!") - >>> import math - >>> math.floor( 1.9 ) - 1 - """ - pass + >>> assert "Easy!" + >>> import math + >>> math.floor(1.9) + 1 + """ + pass # Tests that long lines get wrapped... appropriately. @@ -2783,59 +7009,61 @@ def doctest_varying_start_column(): # would be to expose another formatter option for controlling the # line-width of code snippets independently. def doctest_long_lines(): - """ - Do cool stuff. + """ + 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) + 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 - ) + 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. + """ + Do cool stuff. - >>> cool-stuff( x ): - 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool-stuff( x ): - ... print( f"hi {x}" ); - 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 - """ - pass + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass # Checks that a doctest with some proper indentation and some improper @@ -2843,34 +7071,34 @@ def doctest_skipped_inconsistent_indent(): # 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. + """ + Do cool stuff. - >>> def cool_stuff( x ): - ... print( x ) - ... print( f"hi {x}" ); - hi 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> foo( x ) - ... '''tri'''cksy''' - """ - pass + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass # Tests that a doctest on a single line is skipped. def doctest_skipped_one_line(): - ">>> foo( x )" - pass + ">>> foo( x )" + pass # f-strings are not considered docstrings[1], so any doctests @@ -2878,25 +7106,25 @@ def doctest_skipped_one_line(): # # [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals def doctest_skipped_fstring(): - f""" + f""" Do cool stuff. >>> cool_stuff( 1 ) 2 """ - pass + 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. + """ + Do cool stuff. - >>> x = '\"\"\"' - """ - pass + >>> x = '\"\"\"' + """ + pass ############################################################################### @@ -2914,125 +7142,128 @@ def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): def rst_literal_simple(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass def rst_literal_simple_continued(): - """ - Do cool stuff:: + """ + Do cool stuff:: - def cool_stuff( x ): - print( f"hi {x}" ); + def cool_stuff(x): + print(f"hi {x}") - Done. - """ - pass + Done. + """ + pass # Tests that we can end the literal block on the second # to last line of the docstring. def rst_literal_second_to_last(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) - """ - pass + cool_stuff(1) + """ + pass # Tests that we can end the literal block on the actual # last line of the docstring. def rst_literal_actually_last(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 )""" - pass + cool_stuff(1)""" + pass def rst_literal_with_blank_lines(): - """ - Do cool stuff:: + """ + Do cool stuff:: - def cool_stuff( x ): - print( f"hi {x}" ); + def cool_stuff(x): + print(f"hi {x}") - def other_stuff( y ): - print( y ) - Done. - """ - pass + def other_stuff(y): + print(y) + + Done. + """ + pass # Extra blanks should be preserved. def rst_literal_extra_blanks(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass # If a literal block is never properly ended (via a non-empty unindented line), # then the end of the block should be the last non-empty line. And subsequent # empty lines should be preserved as-is. def rst_literal_extra_blanks_at_end(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff(1) - """ - pass + """ + pass # A literal block can contain many empty lines and it should not end the block # if it continues. def rst_literal_extra_blanks_in_snippet(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff(1) - cool_stuff( 2 ) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # This tests that a unindented line appearing after an indented line (but where # the indent is still beyond the minimum) gets formatted properly. def rst_literal_subsequent_line_not_indented(): - """ - Do cool stuff:: + """ + Do cool stuff:: - if True: - cool_stuff( ''' - hiya''' ) + if True: + cool_stuff( + ''' + hiya''' + ) - Done. - """ - pass + Done. + """ + pass # This checks that if the first line in a code snippet has been indented with @@ -3044,27 +7275,27 @@ def rst_literal_subsequent_line_not_indented(): # itself also seems to recognize this as a code block, although it appears # under-specified.) def rst_literal_first_line_indent_uses_tabs_4spaces(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass # Like the test above, but with multiple lines. def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) + cool_stuff(1) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # Another test with tabs, except in this case, if your tabwidth is less than @@ -3075,42 +7306,42 @@ def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): # snippet is actually seen as being more indented than the opening `::` line. # As with the above example, reST seems to behave the same way here. def rst_literal_first_line_indent_uses_tabs_8spaces(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass # Like the test above, but with multiple lines. def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) + cool_stuff(1) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # Tests that if two lines in a literal block are indented to the same level # but by different means (tabs versus spaces), then we correctly recognize the # block and format it. def rst_literal_first_line_tab_second_line_spaces(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) + cool_stuff(1) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # Tests that when two lines in a code snippet have weird and inconsistent @@ -3132,122 +7363,122 @@ def rst_literal_first_line_tab_second_line_spaces(): # but we might want to switch to the alternative if cases like this show up in # the real world. ---AG def rst_literal_odd_indentation(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) + cool_stuff(1) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # Tests that having a line with a lone `::` works as an introduction of a # literal block. def rst_literal_lone_colon(): - """ - Do cool stuff. + """ + Do cool stuff. - :: + :: - cool_stuff( 1 ) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass def rst_directive_simple(): - """ - .. code-block:: python + """ + .. code-block:: python - cool_stuff( 1 ) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass def rst_directive_case_insensitive(): - """ - .. cOdE-bLoCk:: python + """ + .. cOdE-bLoCk:: python - cool_stuff( 1 ) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass def rst_directive_sourcecode(): - """ - .. sourcecode:: python - - cool_stuff( 1 ) - - Done. - """ - pass + """ + .. sourcecode:: python + cool_stuff(1) -def rst_directive_options(): - """ - .. code-block:: python - :linenos: - :emphasize-lines: 2,3 - :name: blah blah + Done. + """ + pass - cool_stuff( 1 ) - cool_stuff( 2 ) - cool_stuff( 3 ) - cool_stuff( 4 ) - Done. - """ - pass +def rst_directive_options(): + """ + .. code-block:: python + :linenos: + :emphasize-lines: 2,3 + :name: blah blah + + cool_stuff(1) + cool_stuff(2) + cool_stuff(3) + cool_stuff(4) + + Done. + """ + pass # In this case, since `pycon` isn't recognized as a Python code snippet, the # docstring reformatter ignores it. But it then picks up the doctest and # reformats it. def rst_directive_doctest(): - """ - .. code-block:: pycon + """ + .. code-block:: pycon - >>> cool_stuff( 1 ) + >>> cool_stuff(1) - Done. - """ - pass + Done. + """ + pass # This checks that if the first non-empty line after the start of a literal # block is not indented more than the line containing the `::`, then it is not # treated as a code snippet. def rst_literal_skipped_first_line_not_indented(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # Like the test above, but inserts an indented line after the un-indented one. # This should not cause the literal block to be resumed. def rst_literal_skipped_first_line_not_indented_then_indented(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) + cool_stuff( 1 ) + cool_stuff( 2 ) - Done. - """ - pass + Done. + """ + pass # This also checks that a code snippet is not reformatted when the indentation @@ -3256,27 +7487,27 @@ def rst_literal_skipped_first_line_not_indented_then_indented(): # length is computed by assuming a tabwidth equal to 8. reST also rejects this # and doesn't treat it as a literal block. def rst_literal_skipped_first_line_not_indented_tab(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # Like the previous test, but adds a second line. def rst_literal_skipped_first_line_not_indented_tab_multiple(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) + cool_stuff( 1 ) + cool_stuff( 2 ) - Done. - """ - pass + Done. + """ + pass # Tests that a code block with a second line that is not properly indented gets @@ -3285,16 +7516,16 @@ def rst_literal_skipped_first_line_not_indented_tab_multiple(): # One trick here is that we need to make sure the Python code in the snippet is # valid, otherwise it would be skipped because of invalid Python. def rst_literal_skipped_subsequent_line_not_indented(): - """ - Do cool stuff:: + """ + Do cool stuff:: - if True: - cool_stuff( ''' - hiya''' ) + if True: + cool_stuff( ''' + hiya''' ) - Done. - """ - pass + Done. + """ + pass # In this test, we write what looks like a code-block, but it should be treated @@ -3304,14 +7535,14 @@ def rst_literal_skipped_subsequent_line_not_indented(): # rules, but we currently consider the `.. ` prefix to indicate that it is not # a literal block. def rst_literal_skipped_not_directive(): - """ - .. code-block:: + """ + .. code-block:: - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # In this test, we start a line with `.. `, which makes it look like it might @@ -3322,15 +7553,15 @@ def rst_literal_skipped_not_directive(): # code snippet. The reST render I was using to test things does actually treat # this as a code block, so we may be out of conformance here. def rst_literal_skipped_possible_false_negative(): - """ - This is a test. - .. This is a test:: + """ + This is a test. + .. This is a test:: - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # This tests that a doctest inside of a reST literal block doesn't get @@ -3338,1758 +7569,1905 @@ def rst_literal_skipped_possible_false_negative(): # like it might be the right behavior since it is a literal block. (The doctest # makes the Python code invalid.) def rst_literal_skipped_doctest(): - """ - Do cool stuff:: + """ + Do cool stuff:: - >>> cool_stuff( 1 ) + >>> cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass + + +def rst_literal_skipped_markdown(): + """ + Do cool stuff:: + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass def rst_directive_skipped_not_indented(): - """ - .. code-block:: python + """ + .. code-block:: python - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass def rst_directive_skipped_wrong_language(): - """ - .. code-block:: rust + """ + .. code-block:: rust - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # This gets skipped for the same reason that the doctest in a literal block # gets skipped. def rst_directive_skipped_doctest(): - """ - .. code-block:: python + """ + .. code-block:: python + + >>> cool_stuff( 1 ) + + Done. + """ + pass + + +############################################################################### +# Markdown CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Markdown fenced code blocks. +# +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks +############################################################################### + + +def markdown_simple(): + """ + Do cool stuff. + + ```py + cool_stuff(1) + ``` + + Done. + """ + pass + + +def markdown_simple_continued(): + """ + Do cool stuff. + + ```python + def cool_stuff(x): + print(f"hi {x}") + ``` + + Done. + """ + pass + + +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): + """ + Do cool stuff. + + ``` + cool_stuff(1) + ``` + + Done. + """ + pass + + +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): + """ + Do cool stuff. + + ~~~py + cool_stuff(1) + ~~~ + + Done. + """ + pass + + +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff(1) + `````` + + Done. + """ + pass + + +# Tests that an invalid closing fence is treated as invalid. +# +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + + ```py + cool_stuff(1) + ''' + ```invalid + ''' + cool_stuff(2) + ``` + + Done. + """ + pass + + +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): + """ + Do cool stuff. + + `````` + do_something( + ''' + ``` + did i trick you? + ``` + ''' + ) + `````` - >>> cool_stuff( 1 ) + Done. + """ + pass - Done. - """ - pass -``` +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): + """ + Do cool stuff. -### 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 -``` + ```py + cool_stuff(1)""" + pass -```python -############################################################################### -# DOCTEST CODE EXAMPLES -# -# This section shows examples of docstrings that contain code snippets in -# Python's "doctest" format. -# -# See: https://docs.python.org/3/library/doctest.html -############################################################################### -# The simplest doctest to ensure basic formatting works. -def doctest_simple(): - """ - Do cool stuff. +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): + """ + Do cool stuff. - >>> cool_stuff( 1 ) - 2 - """ - pass + ```py + cool_stuff(1) + ``` + """ + pass -# Another simple test, but one where the Python code -# extends over multiple lines. -def doctest_simple_continued(): - """ - Do cool stuff. +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): + """ + Do cool stuff. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 - """ - pass + ```py + cool_stuff(1)""" + pass -# Test that we support multiple directly adjacent -# doctests. -def doctest_adjacent(): - """ - Do cool stuff. +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): + """ + Do cool stuff. - >>> cool_stuff( x ) - >>> cool_stuff( y ) - 2 - """ - pass + ```py + cool_stuff(1) + ```""" + pass -# Test that a doctest on the last non-whitespace line of a docstring -# reformats correctly. -def doctest_last_line(): - """ - Do cool stuff. +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): + """ + Do cool stuff. - >>> cool_stuff( x ) - """ - pass + ```py + cool_stuff(1)""" + 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 markdown_with_blank_lines(): + """ + Do cool stuff. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - """ - pass + ```py + def cool_stuff(x): + print(f"hi {x}") -# Test that a doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line(): - """ - Do cool stuff. + def other_stuff(y): + print(y) + ``` - >>> cool_stuff( x )""" - pass + Done. + """ + pass -# Test that a continued doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line_continued(): - """ - Do cool stuff. +def markdown_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff. - >>> cool_stuff( x ) - ... more( y )""" - pass + ```py + cool_stuff(1) + ``` + Done. + """ + 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 +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff. + ```py + cool_stuff(1) + cool_stuff(2) + ``` -# 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. + Done. + """ + pass - >>> def cool_stuff ( x ): - ... print( x ) - ... print( x ) - ... - """ - pass +def markdown_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff. -# 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. + ```py + cool_stuff(1) + ``` - >>> def cool_stuff ( x ): - ... print( x ) - ... print( x ) - ... + Done. + """ + pass - And say something else. - """ - pass +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff. -# 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. + ```py + cool_stuff(1) + cool_stuff(2) + ``` - >>> x = '''tricksy''' - """ - pass + Done. + """ + 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. +def markdown_first_line_tab_second_line_spaces(): + """ + Do cool stuff. - >>> x = f'''tricksy''' - """ - pass + ```py + cool_stuff(1) + cool_stuff(2) + ``` + Done. + """ + 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 +def markdown_odd_indentation(): + """ + Do cool stuff. + ```py + cool_stuff(1) + cool_stuff(2) + ``` -# Tests that inverting the triple quoting works as expected. -def doctest_with_triple_inverted(): - ''' - Do cool stuff. + Done. + """ + pass - >>> x = """tricksy""" - ''' - pass +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): + """ + Do cool stuff. -# Tests that inverting the triple quoting with an f-string works as -# expected. -def doctest_with_triple_inverted_fstring(): - ''' - Do cool stuff. + ```py + cool_stuff(1) + ``` - >>> x = f"""tricksy""" - ''' - pass + Done. + """ + 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. +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): + """ + Do cool stuff. - >>> def nested( x ): - ... """ - ... Do nested cool stuff. - ... >>> func_call( 5 ) - ... """ - ... pass - ''' - pass + ```py + cool_stuff(1) -# Tests that the starting column does not matter. -def doctest_varying_start_column(): - """ - Do cool stuff. + cool_stuff(2) + ``` - >>> assert ("Easy!") - >>> import math - >>> math.floor( 1.9 ) - 1 - """ - pass + Done. + """ + 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. +def markdown_weird_closing(): + """ + Code block with weirdly placed closing fences. - 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) + ```python + cool_stuff(1) + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` - 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 - ) + Now the code block is closed + """ + pass -# Checks that a simple but invalid doctest gets skipped. -def doctest_skipped_simple(): - """ - Do cool stuff. +def markdown_over_indented(): + """ + A docstring + over intended + ```python + print(5) + ``` + """ + pass - >>> cool-stuff( x ): - 2 - """ - pass +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): + """ + Do cool stuff. -# Checks that a simple doctest that is continued over multiple lines, -# but is invalid, gets skipped. -def doctest_skipped_simple_continued(): - """ - Do cool stuff. + ```py + cool_stuff( 1 ) - >>> def cool-stuff( x ): - ... print( f"hi {x}" ); - 2 - """ - pass + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. + """ + pass -# Checks that a doctest with improper indentation gets skipped. -def doctest_skipped_inconsistent_indent(): - """ - Do cool stuff. +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): + """ + Do cool stuff. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 - """ - pass + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` + Done. + """ + 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 +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. +# +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): + """ + Do cool stuff. + ```py + cool_stuff( 1 ) + ``` -# 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. + Done. + """ + pass - >>> foo( x ) - ... '''tri'''cksy''' - """ - pass +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): + """ + Do cool stuff. -# Tests that a doctest on a single line is skipped. -def doctest_skipped_one_line(): - ">>> foo( x )" - pass + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass -# f-strings are not considered docstrings[1], so any doctests -# inside of them should not be formatted. +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. # -# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals -def doctest_skipped_fstring(): - f""" +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): + """ Do cool stuff. - >>> cool_stuff( 1 ) - 2 + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` + + Done. """ - pass + 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. +def markdown_skipped_doctest(): + """ + Do cool stuff. - >>> x = '\"\"\"' - """ - pass + ```py + >>> cool_stuff( 1 ) + ``` + Done. + """ + pass -############################################################################### -# reStructuredText CODE EXAMPLES -# -# This section shows examples of docstrings that contain code snippets in -# reStructuredText formatted code blocks. -# -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 -############################################################################### +def markdown_skipped_rst_literal(): + """ + Do cool stuff. -def rst_literal_simple(): - """ - Do cool stuff:: + ```py + And do this:: - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + ``` + Done. + """ + pass -def rst_literal_simple_continued(): - """ - Do cool stuff:: - def cool_stuff( x ): - print( f"hi {x}" ); +def markdown_skipped_rst_directive(): + """ + Do cool stuff. - Done. - """ - pass + ```py + .. code-block:: python + cool_stuff( 1 ) -# Tests that we can end the literal block on the second -# to last line of the docstring. -def rst_literal_second_to_last(): - """ - Do cool stuff:: + ``` - cool_stuff( 1 ) - """ - pass + Done. + """ + pass +``` -# Tests that we can end the literal block on the actual -# last line of the docstring. -def rst_literal_actually_last(): - """ - Do cool stuff:: +### 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 +``` - cool_stuff( 1 )""" - pass +```python +############################################################################### +# DOCTEST CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Python's "doctest" format. +# +# See: https://docs.python.org/3/library/doctest.html +############################################################################### +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): + """ + Do cool stuff. -def rst_literal_with_blank_lines(): - """ - Do cool stuff:: + >>> cool_stuff(1) + 2 + """ + pass - def cool_stuff( x ): - print( f"hi {x}" ); - def other_stuff( y ): - print( y ) +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. - Done. - """ - pass + >>> def cool_stuff(x): + ... print(f"hi {x}") + hi 2 + """ + pass -# Extra blanks should be preserved. -def rst_literal_extra_blanks(): - """ - Do cool stuff:: +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): + """ + Do cool stuff. + >>> cool_stuff(x) + >>> cool_stuff(y) + 2 + """ + pass - cool_stuff( 1 ) +# 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 - Done. - """ - 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 -# If a literal block is never properly ended (via a non-empty unindented line), -# then the end of the block should be the last non-empty line. And subsequent -# empty lines should be preserved as-is. -def rst_literal_extra_blanks_at_end(): - """ - Do cool stuff:: +# Test that a doctest on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line(): + """ + Do cool stuff. - cool_stuff( 1 ) + >>> cool_stuff(x)""" + pass +# Test that a continued doctest on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line_continued(): + """ + Do cool stuff. - """ - pass + >>> cool_stuff(x) + ... more(y)""" + pass -# A literal block can contain many empty lines and it should not end the block -# if it continues. -def rst_literal_extra_blanks_in_snippet(): - """ - Do cool stuff:: +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): + """ + Do cool stuff. - cool_stuff( 1 ) + >>> def cool_stuff(x): + ... print(x) + ... + ... print(x) + """ + pass - cool_stuff( 2 ) +# 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. - Done. - """ - pass + >>> def cool_stuff(x): + ... print(x) + ... print(x) + """ + pass -# This tests that a unindented line appearing after an indented line (but where -# the indent is still beyond the minimum) gets formatted properly. -def rst_literal_subsequent_line_not_indented(): - """ - Do cool stuff:: +# 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. - if True: - cool_stuff( ''' - hiya''' ) + >>> def cool_stuff(x): + ... print(x) + ... print(x) - Done. - """ - pass + And say something else. + """ + pass -# This checks that if the first line in a code snippet has been indented with -# tabs, then so long as its "indentation length" is considered bigger than the -# line with `::`, it is reformatted as code. -# -# (If your tabwidth is set to 4, then it looks like the code snippet -# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST -# itself also seems to recognize this as a code block, although it appears -# under-specified.) -def rst_literal_first_line_indent_uses_tabs_4spaces(): - """ - Do cool stuff:: +# 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. - cool_stuff( 1 ) + >>> x = '''tricksy''' + """ + pass - Done. - """ - 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. -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): - """ - Do cool stuff:: + >>> x = f'''tricksy''' + """ + pass - cool_stuff( 1 ) - cool_stuff( 2 ) - Done. - """ - 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 -# Another test with tabs, except in this case, if your tabwidth is less than -# 8, than the code snippet actually looks like its indent is *less* than the -# opening line with a `::`. One might presume this means that the code snippet -# is not treated as a literal block and thus not reformatted, but since we -# assume all tabs have tabwidth=8 when computing indentation length, the code -# snippet is actually seen as being more indented than the opening `::` line. -# As with the above example, reST seems to behave the same way here. -def rst_literal_first_line_indent_uses_tabs_8spaces(): - """ - Do cool stuff:: - cool_stuff( 1 ) +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. - Done. - """ - pass + >>> x = """tricksy""" + ''' + pass -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): - """ - Do cool stuff:: +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. - cool_stuff( 1 ) - cool_stuff( 2 ) + >>> x = f"""tricksy""" + ''' + pass - Done. - """ - 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. -# Tests that if two lines in a literal block are indented to the same level -# but by different means (tabs versus spaces), then we correctly recognize the -# block and format it. -def rst_literal_first_line_tab_second_line_spaces(): - """ - Do cool stuff:: + >>> def nested(x): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' + pass - cool_stuff( 1 ) - cool_stuff( 2 ) - Done. - """ - 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 when two lines in a code snippet have weird and inconsistent -# indentation, the code still gets formatted so long as the indent is greater -# than the indent of the `::` line. -# -# In this case, the minimum indent is 5 spaces (from the second line) where as -# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). -# The minimum indent is stripped from each code line. Since tabs aren't -# divisible, the entire tab is stripped, which means the first and second lines -# wind up with the same level of indentation. -# -# An alternative behavior here would be that the tab is replaced with 3 spaces -# instead of being stripped entirely. The code snippet itself would then have -# inconsistent indentation to the point of being invalid Python, and thus code -# formatting would be skipped. -# -# I decided on the former behavior because it seems a bit easier to implement, -# but we might want to switch to the alternative if cases like this show up in -# the real world. ---AG -def rst_literal_odd_indentation(): - """ - Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) +# 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. - Done. - """ - pass + 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 + ) -# Tests that having a line with a lone `::` works as an introduction of a -# literal block. -def rst_literal_lone_colon(): - """ - Do cool stuff. - :: +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): + """ + Do cool stuff. - cool_stuff( 1 ) + >>> cool-stuff( x ): + 2 + """ + pass - Done. - """ - 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 rst_directive_simple(): - """ - .. code-block:: python + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 + """ + pass - cool_stuff( 1 ) - Done. - """ - 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 -def rst_directive_case_insensitive(): - """ - .. cOdE-bLoCk:: python - cool_stuff( 1 ) +# 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. - Done. - """ - pass + >>> def cool_stuff(x): + ... print(x) + ... print( f"hi {x}" ); + hi 2 + """ + pass -def rst_directive_sourcecode(): - """ - .. sourcecode:: python +# 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. - cool_stuff( 1 ) + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass - Done. - """ - pass +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" + pass -def rst_directive_options(): - """ - .. code-block:: python - :linenos: - :emphasize-lines: 2,3 - :name: blah blah - cool_stuff( 1 ) - cool_stuff( 2 ) - cool_stuff( 3 ) - cool_stuff( 4 ) +# 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. - Done. - """ - pass + >>> cool_stuff( 1 ) + 2 + """ + pass -# In this case, since `pycon` isn't recognized as a Python code snippet, the -# docstring reformatter ignores it. But it then picks up the doctest and -# reformats it. -def rst_directive_doctest(): - """ - .. code-block:: pycon +# 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. - >>> cool_stuff( 1 ) + >>> x = '\"\"\"' + """ + pass - Done. - """ - pass +############################################################################### +# reStructuredText CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# reStructuredText formatted code blocks. +# +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 +############################################################################### -# This checks that if the first non-empty line after the start of a literal -# block is not indented more than the line containing the `::`, then it is not -# treated as a code snippet. -def rst_literal_skipped_first_line_not_indented(): - """ - Do cool stuff:: - cool_stuff( 1 ) +def rst_literal_simple(): + """ + Do cool stuff:: - Done. - """ - pass + cool_stuff(1) + Done. + """ + pass -# Like the test above, but inserts an indented line after the un-indented one. -# This should not cause the literal block to be resumed. -def rst_literal_skipped_first_line_not_indented_then_indented(): - """ - Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) +def rst_literal_simple_continued(): + """ + Do cool stuff:: - Done. - """ - pass + def cool_stuff(x): + print(f"hi {x}") + Done. + """ + pass -# This also checks that a code snippet is not reformatted when the indentation -# of the first line is not more than the line with `::`, but this uses tabs to -# make it a little more confounding. It relies on the fact that indentation -# length is computed by assuming a tabwidth equal to 8. reST also rejects this -# and doesn't treat it as a literal block. -def rst_literal_skipped_first_line_not_indented_tab(): - """ - Do cool stuff:: - cool_stuff( 1 ) +# Tests that we can end the literal block on the second +# to last line of the docstring. +def rst_literal_second_to_last(): + """ + Do cool stuff:: - Done. - """ - pass + cool_stuff(1) + """ + pass -# Like the previous test, but adds a second line. -def rst_literal_skipped_first_line_not_indented_tab_multiple(): - """ - Do cool stuff:: +# Tests that we can end the literal block on the actual +# last line of the docstring. +def rst_literal_actually_last(): + """ + Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) + cool_stuff(1)""" + pass - Done. - """ - pass +def rst_literal_with_blank_lines(): + """ + Do cool stuff:: -# Tests that a code block with a second line that is not properly indented gets -# skipped. A valid code block needs to have an empty line separating these. -# -# One trick here is that we need to make sure the Python code in the snippet is -# valid, otherwise it would be skipped because of invalid Python. -def rst_literal_skipped_subsequent_line_not_indented(): - """ - Do cool stuff:: + def cool_stuff(x): + print(f"hi {x}") - if True: - cool_stuff( ''' - hiya''' ) - Done. - """ - pass + def other_stuff(y): + print(y) + Done. + """ + pass -# In this test, we write what looks like a code-block, but it should be treated -# as invalid due to the missing `language` argument. -# -# It does still look like it could be a literal block according to the literal -# rules, but we currently consider the `.. ` prefix to indicate that it is not -# a literal block. -def rst_literal_skipped_not_directive(): - """ - .. code-block:: - cool_stuff( 1 ) +# Extra blanks should be preserved. +def rst_literal_extra_blanks(): + """ + Do cool stuff:: - Done. - """ - pass -# In this test, we start a line with `.. `, which makes it look like it might -# be a directive. But instead continue it as if it was just some periods from -# the previous line, and then try to end it by starting a literal block. -# -# But because of the `.. ` in the beginning, we wind up not treating this as a -# code snippet. The reST render I was using to test things does actually treat -# this as a code block, so we may be out of conformance here. -def rst_literal_skipped_possible_false_negative(): - """ - This is a test. - .. This is a test:: + cool_stuff(1) - cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass -# This tests that a doctest inside of a reST literal block doesn't get -# reformatted. It's plausible this isn't the right behavior, but it also seems -# like it might be the right behavior since it is a literal block. (The doctest -# makes the Python code invalid.) -def rst_literal_skipped_doctest(): - """ - Do cool stuff:: - >>> cool_stuff( 1 ) +# If a literal block is never properly ended (via a non-empty unindented line), +# then the end of the block should be the last non-empty line. And subsequent +# empty lines should be preserved as-is. +def rst_literal_extra_blanks_at_end(): + """ + Do cool stuff:: - Done. - """ - pass + cool_stuff(1) -def rst_directive_skipped_not_indented(): - """ - .. code-block:: python - cool_stuff( 1 ) - Done. - """ - pass + """ + pass -def rst_directive_skipped_wrong_language(): - """ - .. code-block:: rust +# A literal block can contain many empty lines and it should not end the block +# if it continues. +def rst_literal_extra_blanks_in_snippet(): + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff(1) - Done. - """ - pass + cool_stuff(2) -# This gets skipped for the same reason that the doctest in a literal block -# gets skipped. -def rst_directive_skipped_doctest(): - """ - .. code-block:: python + Done. + """ + pass - >>> cool_stuff( 1 ) - Done. - """ - pass -``` +# This tests that a unindented line appearing after an indented line (but where +# the indent is still beyond the minimum) gets formatted properly. +def rst_literal_subsequent_line_not_indented(): + """ + Do cool stuff:: + if True: + cool_stuff( + ''' + hiya''' + ) -### 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 -``` + Done. + """ + pass -```python -############################################################################### -# DOCTEST CODE EXAMPLES -# -# This section shows examples of docstrings that contain code snippets in -# Python's "doctest" format. + +# This checks that if the first line in a code snippet has been indented with +# tabs, then so long as its "indentation length" is considered bigger than the +# line with `::`, it is reformatted as code. # -# See: https://docs.python.org/3/library/doctest.html -############################################################################### +# (If your tabwidth is set to 4, then it looks like the code snippet +# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST +# itself also seems to recognize this as a code block, although it appears +# under-specified.) +def rst_literal_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff:: -# The simplest doctest to ensure basic formatting works. -def doctest_simple(): - """ - Do cool stuff. + cool_stuff(1) - >>> cool_stuff(1) - 2 - """ - pass + Done. + """ + pass -# Another simple test, but one where the Python code -# extends over multiple lines. -def doctest_simple_continued(): - """ - Do cool stuff. +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff:: - >>> def cool_stuff(x): - ... print(f"hi {x}") - hi 2 - """ - pass + cool_stuff(1) + cool_stuff(2) + Done. + """ + pass -# Test that we support multiple directly adjacent -# doctests. -def doctest_adjacent(): - """ - Do cool stuff. - >>> cool_stuff(x) - >>> cool_stuff(y) - 2 - """ - pass +# Another test with tabs, except in this case, if your tabwidth is less than +# 8, than the code snippet actually looks like its indent is *less* than the +# opening line with a `::`. One might presume this means that the code snippet +# is not treated as a literal block and thus not reformatted, but since we +# assume all tabs have tabwidth=8 when computing indentation length, the code +# snippet is actually seen as being more indented than the opening `::` line. +# As with the above example, reST seems to behave the same way here. +def rst_literal_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff:: + cool_stuff(1) -# Test that a doctest on the last non-whitespace line of a docstring -# reformats correctly. -def doctest_last_line(): - """ - Do cool stuff. + Done. + """ + pass - >>> cool_stuff(x) - """ - pass +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff:: -# 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. + cool_stuff(1) + cool_stuff(2) - >>> def cool_stuff(x): - ... print(f"hi {x}") - """ - pass + Done. + """ + pass -# Test that a doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line(): - """ - Do cool stuff. +# Tests that if two lines in a literal block are indented to the same level +# but by different means (tabs versus spaces), then we correctly recognize the +# block and format it. +def rst_literal_first_line_tab_second_line_spaces(): + """ + Do cool stuff:: - >>> cool_stuff(x)""" - pass + cool_stuff(1) + cool_stuff(2) + Done. + """ + pass + + +# Tests that when two lines in a code snippet have weird and inconsistent +# indentation, the code still gets formatted so long as the indent is greater +# than the indent of the `::` line. +# +# In this case, the minimum indent is 5 spaces (from the second line) where as +# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). +# The minimum indent is stripped from each code line. Since tabs aren't +# divisible, the entire tab is stripped, which means the first and second lines +# wind up with the same level of indentation. +# +# An alternative behavior here would be that the tab is replaced with 3 spaces +# instead of being stripped entirely. The code snippet itself would then have +# inconsistent indentation to the point of being invalid Python, and thus code +# formatting would be skipped. +# +# I decided on the former behavior because it seems a bit easier to implement, +# but we might want to switch to the alternative if cases like this show up in +# the real world. ---AG +def rst_literal_odd_indentation(): + """ + Do cool stuff:: -# Test that a continued doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line_continued(): - """ - Do cool stuff. + cool_stuff(1) + cool_stuff(2) - >>> cool_stuff(x) - ... more(y)""" - pass + Done. + """ + pass -# Test that a doctest is correctly identified and formatted with a blank -# continuation line. -def doctest_blank_continued(): - """ - Do cool stuff. +# Tests that having a line with a lone `::` works as an introduction of a +# literal block. +def rst_literal_lone_colon(): + """ + Do cool stuff. - >>> def cool_stuff(x): - ... print(x) - ... - ... print(x) - """ - pass + :: + cool_stuff(1) -# 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. + Done. + """ + pass - >>> def cool_stuff(x): - ... print(x) - ... print(x) - """ - pass +def rst_directive_simple(): + """ + .. code-block:: python -# 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. + cool_stuff(1) - >>> def cool_stuff(x): - ... print(x) - ... print(x) + Done. + """ + pass - And say something else. - """ - pass +def rst_directive_case_insensitive(): + """ + .. cOdE-bLoCk:: python -# 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. + cool_stuff(1) - >>> x = '''tricksy''' - """ - pass + Done. + """ + 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. +def rst_directive_sourcecode(): + """ + .. sourcecode:: python - >>> x = f'''tricksy''' - """ - pass + cool_stuff(1) + Done. + """ + 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 +def rst_directive_options(): + """ + .. code-block:: python + :linenos: + :emphasize-lines: 2,3 + :name: blah blah + cool_stuff(1) + cool_stuff(2) + cool_stuff(3) + cool_stuff(4) -# Tests that inverting the triple quoting works as expected. -def doctest_with_triple_inverted(): - ''' - Do cool stuff. + Done. + """ + pass - >>> x = """tricksy""" - ''' - pass +# In this case, since `pycon` isn't recognized as a Python code snippet, the +# docstring reformatter ignores it. But it then picks up the doctest and +# reformats it. +def rst_directive_doctest(): + """ + .. code-block:: pycon -# Tests that inverting the triple quoting with an f-string works as -# expected. -def doctest_with_triple_inverted_fstring(): - ''' - Do cool stuff. + >>> cool_stuff(1) - >>> x = f"""tricksy""" - ''' - pass + Done. + """ + 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. +# This checks that if the first non-empty line after the start of a literal +# block is not indented more than the line containing the `::`, then it is not +# treated as a code snippet. +def rst_literal_skipped_first_line_not_indented(): + """ + Do cool stuff:: - >>> def nested(x): - ... """ - ... Do nested cool stuff. - ... >>> func_call( 5 ) - ... """ - ... pass - ''' - pass + cool_stuff( 1 ) + Done. + """ + 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 +# Like the test above, but inserts an indented line after the un-indented one. +# This should not cause the literal block to be resumed. +def rst_literal_skipped_first_line_not_indented_then_indented(): + """ + Do cool stuff:: + cool_stuff( 1 ) + cool_stuff( 2 ) -# 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. + Done. + """ + pass - 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 - ) +# This also checks that a code snippet is not reformatted when the indentation +# of the first line is not more than the line with `::`, but this uses tabs to +# make it a little more confounding. It relies on the fact that indentation +# length is computed by assuming a tabwidth equal to 8. reST also rejects this +# and doesn't treat it as a literal block. +def rst_literal_skipped_first_line_not_indented_tab(): + """ + Do cool stuff:: + cool_stuff( 1 ) -# Checks that a simple but invalid doctest gets skipped. -def doctest_skipped_simple(): - """ - Do cool stuff. + Done. + """ + pass - >>> cool-stuff( x ): - 2 - """ - pass +# Like the previous test, but adds a second line. +def rst_literal_skipped_first_line_not_indented_tab_multiple(): + """ + Do cool stuff:: -# Checks that a simple doctest that is continued over multiple lines, -# but is invalid, gets skipped. -def doctest_skipped_simple_continued(): - """ - Do cool stuff. + cool_stuff( 1 ) + cool_stuff( 2 ) - >>> def cool-stuff( x ): - ... print( f"hi {x}" ); - 2 - """ - pass + Done. + """ + pass -# Checks that a doctest with improper indentation gets skipped. -def doctest_skipped_inconsistent_indent(): - """ - Do cool stuff. +# Tests that a code block with a second line that is not properly indented gets +# skipped. A valid code block needs to have an empty line separating these. +# +# One trick here is that we need to make sure the Python code in the snippet is +# valid, otherwise it would be skipped because of invalid Python. +def rst_literal_skipped_subsequent_line_not_indented(): + """ + Do cool stuff:: - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 - """ - pass + if True: + cool_stuff( ''' + hiya''' ) + Done. + """ + 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 +# In this test, we write what looks like a code-block, but it should be treated +# as invalid due to the missing `language` argument. +# +# It does still look like it could be a literal block according to the literal +# rules, but we currently consider the `.. ` prefix to indicate that it is not +# a literal block. +def rst_literal_skipped_not_directive(): + """ + .. code-block:: + cool_stuff( 1 ) -# 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. + Done. + """ + pass - >>> foo( x ) - ... '''tri'''cksy''' - """ - pass +# In this test, we start a line with `.. `, which makes it look like it might +# be a directive. But instead continue it as if it was just some periods from +# the previous line, and then try to end it by starting a literal block. +# +# But because of the `.. ` in the beginning, we wind up not treating this as a +# code snippet. The reST render I was using to test things does actually treat +# this as a code block, so we may be out of conformance here. +def rst_literal_skipped_possible_false_negative(): + """ + This is a test. + .. This is a test:: -# Tests that a doctest on a single line is skipped. -def doctest_skipped_one_line(): - ">>> foo( x )" - pass + cool_stuff( 1 ) + Done. + """ + 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 +# This tests that a doctest inside of a reST literal block doesn't get +# reformatted. It's plausible this isn't the right behavior, but it also seems +# like it might be the right behavior since it is a literal block. (The doctest +# makes the Python code invalid.) +def rst_literal_skipped_doctest(): + """ + Do cool stuff:: + >>> cool_stuff( 1 ) -# 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. + Done. + """ + pass - >>> x = '\"\"\"' - """ - pass +def rst_literal_skipped_markdown(): + """ + Do cool stuff:: -############################################################################### -# reStructuredText CODE EXAMPLES -# -# This section shows examples of docstrings that contain code snippets in -# reStructuredText formatted code blocks. -# -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 -############################################################################### + ```py + cool_stuff( 1 ) + ``` + Done. + """ + pass -def rst_literal_simple(): - """ - Do cool stuff:: - cool_stuff(1) +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python - Done. - """ - pass + cool_stuff( 1 ) + Done. + """ + pass -def rst_literal_simple_continued(): - """ - Do cool stuff:: - def cool_stuff(x): - print(f"hi {x}") +def rst_directive_skipped_wrong_language(): + """ + .. code-block:: rust - Done. - """ - pass + cool_stuff( 1 ) + Done. + """ + pass -# Tests that we can end the literal block on the second -# to last line of the docstring. -def rst_literal_second_to_last(): - """ - Do cool stuff:: - cool_stuff(1) - """ - pass +# This gets skipped for the same reason that the doctest in a literal block +# gets skipped. +def rst_directive_skipped_doctest(): + """ + .. code-block:: python + >>> cool_stuff( 1 ) -# Tests that we can end the literal block on the actual -# last line of the docstring. -def rst_literal_actually_last(): - """ - Do cool stuff:: + Done. + """ + pass - cool_stuff(1)""" - pass + +############################################################################### +# Markdown CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Markdown fenced code blocks. +# +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks +############################################################################### -def rst_literal_with_blank_lines(): - """ - Do cool stuff:: +def markdown_simple(): + """ + Do cool stuff. - def cool_stuff(x): - print(f"hi {x}") + ```py + cool_stuff(1) + ``` + Done. + """ + pass - def other_stuff(y): - print(y) - Done. - """ - pass +def markdown_simple_continued(): + """ + Do cool stuff. + ```python + def cool_stuff(x): + print(f"hi {x}") + ``` -# Extra blanks should be preserved. -def rst_literal_extra_blanks(): - """ - Do cool stuff:: + Done. + """ + pass +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): + """ + Do cool stuff. - cool_stuff(1) + ``` + cool_stuff(1) + ``` + Done. + """ + pass - Done. - """ - pass +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): + """ + Do cool stuff. + ~~~py + cool_stuff(1) + ~~~ -# If a literal block is never properly ended (via a non-empty unindented line), -# then the end of the block should be the last non-empty line. And subsequent -# empty lines should be preserved as-is. -def rst_literal_extra_blanks_at_end(): - """ - Do cool stuff:: + Done. + """ + pass - cool_stuff(1) +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): + """ + Do cool stuff. + ```py + cool_stuff(1) + `````` + Done. + """ + pass - """ - pass +# Tests that an invalid closing fence is treated as invalid. +# +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): + """ + Do cool stuff. -# A literal block can contain many empty lines and it should not end the block -# if it continues. -def rst_literal_extra_blanks_in_snippet(): - """ - Do cool stuff:: + ```py + cool_stuff(1) + ''' + ```invalid + ''' + cool_stuff(2) + ``` - cool_stuff(1) + Done. + """ + pass - cool_stuff(2) +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): + """ + Do cool stuff. - Done. - """ - pass + `````` + do_something( + ''' + ``` + did i trick you? + ``` + ''' + ) + `````` + Done. + """ + pass -# This tests that a unindented line appearing after an indented line (but where -# the indent is still beyond the minimum) gets formatted properly. -def rst_literal_subsequent_line_not_indented(): - """ - Do cool stuff:: - if True: - cool_stuff( - ''' - hiya''' - ) +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff(1)""" + pass -# This checks that if the first line in a code snippet has been indented with -# tabs, then so long as its "indentation length" is considered bigger than the -# line with `::`, it is reformatted as code. -# -# (If your tabwidth is set to 4, then it looks like the code snippet -# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST -# itself also seems to recognize this as a code block, although it appears -# under-specified.) -def rst_literal_first_line_indent_uses_tabs_4spaces(): - """ - Do cool stuff:: +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): + """ + Do cool stuff. - cool_stuff(1) + ```py + cool_stuff(1) + ``` + """ + pass - Done. - """ - pass +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): + """ + Do cool stuff. -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): - """ - Do cool stuff:: + ```py + cool_stuff(1)""" + pass - cool_stuff(1) - cool_stuff(2) - Done. - """ - pass +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): + """ + Do cool stuff. + ```py + cool_stuff(1) + ```""" + pass -# Another test with tabs, except in this case, if your tabwidth is less than -# 8, than the code snippet actually looks like its indent is *less* than the -# opening line with a `::`. One might presume this means that the code snippet -# is not treated as a literal block and thus not reformatted, but since we -# assume all tabs have tabwidth=8 when computing indentation length, the code -# snippet is actually seen as being more indented than the opening `::` line. -# As with the above example, reST seems to behave the same way here. -def rst_literal_first_line_indent_uses_tabs_8spaces(): - """ - Do cool stuff:: - cool_stuff(1) +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff(1)""" + pass -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): - """ - Do cool stuff:: +def markdown_with_blank_lines(): + """ + Do cool stuff. - cool_stuff(1) - cool_stuff(2) + ```py + def cool_stuff(x): + print(f"hi {x}") - Done. - """ - pass + def other_stuff(y): + print(y) + ``` -# Tests that if two lines in a literal block are indented to the same level -# but by different means (tabs versus spaces), then we correctly recognize the -# block and format it. -def rst_literal_first_line_tab_second_line_spaces(): - """ - Do cool stuff:: + Done. + """ + pass - cool_stuff(1) - cool_stuff(2) - Done. - """ - pass +def markdown_first_line_indent_uses_tabs_4spaces(): + """ + Do cool stuff. + ```py + cool_stuff(1) + ``` -# Tests that when two lines in a code snippet have weird and inconsistent -# indentation, the code still gets formatted so long as the indent is greater -# than the indent of the `::` line. -# -# In this case, the minimum indent is 5 spaces (from the second line) where as -# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). -# The minimum indent is stripped from each code line. Since tabs aren't -# divisible, the entire tab is stripped, which means the first and second lines -# wind up with the same level of indentation. -# -# An alternative behavior here would be that the tab is replaced with 3 spaces -# instead of being stripped entirely. The code snippet itself would then have -# inconsistent indentation to the point of being invalid Python, and thus code -# formatting would be skipped. -# -# I decided on the former behavior because it seems a bit easier to implement, -# but we might want to switch to the alternative if cases like this show up in -# the real world. ---AG -def rst_literal_odd_indentation(): - """ - Do cool stuff:: + Done. + """ + pass - cool_stuff(1) - cool_stuff(2) - Done. - """ - pass +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): + """ + Do cool stuff. + ```py + cool_stuff(1) + cool_stuff(2) + ``` -# Tests that having a line with a lone `::` works as an introduction of a -# literal block. -def rst_literal_lone_colon(): - """ - Do cool stuff. + Done. + """ + pass - :: - cool_stuff(1) +def markdown_first_line_indent_uses_tabs_8spaces(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff(1) + ``` + Done. + """ + pass -def rst_directive_simple(): - """ - .. code-block:: python - cool_stuff(1) +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff(1) + cool_stuff(2) + ``` + Done. + """ + pass -def rst_directive_case_insensitive(): - """ - .. cOdE-bLoCk:: python - cool_stuff(1) +def markdown_first_line_tab_second_line_spaces(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff(1) + cool_stuff(2) + ``` + Done. + """ + pass -def rst_directive_sourcecode(): - """ - .. sourcecode:: python - cool_stuff(1) +def markdown_odd_indentation(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff(1) + cool_stuff(2) + ``` + Done. + """ + pass -def rst_directive_options(): - """ - .. code-block:: python - :linenos: - :emphasize-lines: 2,3 - :name: blah blah - cool_stuff(1) - cool_stuff(2) - cool_stuff(3) - cool_stuff(4) +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff(1) + ``` + Done. + """ + pass -# In this case, since `pycon` isn't recognized as a Python code snippet, the -# docstring reformatter ignores it. But it then picks up the doctest and -# reformats it. -def rst_directive_doctest(): - """ - .. code-block:: pycon - >>> cool_stuff(1) +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff(1) -# This checks that if the first non-empty line after the start of a literal -# block is not indented more than the line containing the `::`, then it is not -# treated as a code snippet. -def rst_literal_skipped_first_line_not_indented(): - """ - Do cool stuff:: + cool_stuff(2) + ``` - cool_stuff( 1 ) + Done. + """ + pass - Done. - """ - pass +def markdown_weird_closing(): + """ + Code block with weirdly placed closing fences. -# Like the test above, but inserts an indented line after the un-indented one. -# This should not cause the literal block to be resumed. -def rst_literal_skipped_first_line_not_indented_then_indented(): - """ - Do cool stuff:: + ```python + cool_stuff(1) + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` - cool_stuff( 1 ) - cool_stuff( 2 ) + Now the code block is closed + """ + pass - Done. - """ - pass +def markdown_over_indented(): + """ + A docstring + over intended + ```python + print(5) + ``` + """ + pass -# This also checks that a code snippet is not reformatted when the indentation -# of the first line is not more than the line with `::`, but this uses tabs to -# make it a little more confounding. It relies on the fact that indentation -# length is computed by assuming a tabwidth equal to 8. reST also rejects this -# and doesn't treat it as a literal block. -def rst_literal_skipped_first_line_not_indented_tab(): - """ - Do cool stuff:: - cool_stuff( 1 ) +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff( 1 ) + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. + """ + pass -# Like the previous test, but adds a second line. -def rst_literal_skipped_first_line_not_indented_tab_multiple(): - """ - Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): + """ + Do cool stuff. - Done. - """ - pass + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` + + Done. + """ + pass -# Tests that a code block with a second line that is not properly indented gets -# skipped. A valid code block needs to have an empty line separating these. +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. # -# One trick here is that we need to make sure the Python code in the snippet is -# valid, otherwise it would be skipped because of invalid Python. -def rst_literal_skipped_subsequent_line_not_indented(): - """ - Do cool stuff:: +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): + """ + Do cool stuff. - if True: - cool_stuff( ''' - hiya''' ) + ```py + cool_stuff( 1 ) + ``` - Done. - """ - pass + Done. + """ + pass -# In this test, we write what looks like a code-block, but it should be treated -# as invalid due to the missing `language` argument. -# -# It does still look like it could be a literal block according to the literal -# rules, but we currently consider the `.. ` prefix to indicate that it is not -# a literal block. -def rst_literal_skipped_not_directive(): - """ - .. code-block:: +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): + """ + Do cool stuff. - cool_stuff( 1 ) + ```py + cool_stuff( 1 ) + ``` - Done. - """ - pass + Done. + """ + pass -# In this test, we start a line with `.. `, which makes it look like it might -# be a directive. But instead continue it as if it was just some periods from -# the previous line, and then try to end it by starting a literal block. +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. # -# But because of the `.. ` in the beginning, we wind up not treating this as a -# code snippet. The reST render I was using to test things does actually treat -# this as a code block, so we may be out of conformance here. -def rst_literal_skipped_possible_false_negative(): - """ - This is a test. - .. This is a test:: +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): + """ + Do cool stuff. - cool_stuff( 1 ) + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` - Done. - """ - pass + Done. + """ + pass -# This tests that a doctest inside of a reST literal block doesn't get -# reformatted. It's plausible this isn't the right behavior, but it also seems -# like it might be the right behavior since it is a literal block. (The doctest -# makes the Python code invalid.) -def rst_literal_skipped_doctest(): - """ - Do cool stuff:: +def markdown_skipped_doctest(): + """ + Do cool stuff. - >>> cool_stuff( 1 ) + ```py + >>> cool_stuff( 1 ) + ``` - Done. - """ - pass + Done. + """ + pass -def rst_directive_skipped_not_indented(): - """ - .. code-block:: python +def markdown_skipped_rst_literal(): + """ + Do cool stuff. - cool_stuff( 1 ) + ```py + And do this:: - Done. - """ - pass + cool_stuff( 1 ) + ``` -def rst_directive_skipped_wrong_language(): - """ - .. code-block:: rust + Done. + """ + pass - cool_stuff( 1 ) - Done. - """ - pass +def markdown_skipped_rst_directive(): + """ + Do cool stuff. + ```py + .. code-block:: python -# This gets skipped for the same reason that the doctest in a literal block -# gets skipped. -def rst_directive_skipped_doctest(): - """ - .. code-block:: python + cool_stuff( 1 ) - >>> cool_stuff( 1 ) + ``` - Done. - """ - pass + Done. + """ + pass ``` -### Output 6 +### Output 7 ``` -indent-style = space +indent-style = tab line-width = 88 -indent-width = 2 +indent-width = 8 quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect @@ -5109,180 +9487,180 @@ preview = Disabled # The simplest doctest to ensure basic formatting works. def doctest_simple(): - """ - Do cool stuff. + """ + Do cool stuff. - >>> cool_stuff(1) - 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff(x): - ... print(f"hi {x}") - hi 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> cool_stuff(x) - >>> cool_stuff(y) - 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> cool_stuff(x) - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff(x): - ... print(f"hi {x}") - """ - pass + >>> def cool_stuff(x): + ... print(f"hi {x}") + """ + pass # Test that a doctest on the real last line of a docstring reformats # correctly. def doctest_really_last_line(): - """ - Do cool stuff. + """ + Do cool stuff. - >>> cool_stuff(x)""" - pass + >>> cool_stuff(x)""" + pass # Test that a continued doctest on the real last line of a docstring reformats # correctly. def doctest_really_last_line_continued(): - """ - Do cool stuff. + """ + Do cool stuff. - >>> cool_stuff(x) - ... more(y)""" - pass + >>> cool_stuff(x) + ... more(y)""" + pass # Test that a doctest is correctly identified and formatted with a blank # continuation line. def doctest_blank_continued(): - """ - Do cool stuff. + """ + Do cool stuff. - >>> def cool_stuff(x): - ... print(x) - ... - ... print(x) - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff(x): - ... print(x) - ... print(x) - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff(x): - ... print(x) - ... print(x) + >>> def cool_stuff(x): + ... print(x) + ... print(x) - And say something else. - """ - pass + 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. + """ + Do cool stuff. - >>> x = '''tricksy''' - """ - pass + >>> 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. + """ + Do cool stuff. - >>> x = f'''tricksy''' - """ - pass + >>> 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. + """ + Do cool stuff. - >>> x = '''\"\"\"''' - """ - pass + >>> x = '''\"\"\"''' + """ + pass # Tests that inverting the triple quoting works as expected. def doctest_with_triple_inverted(): - ''' - Do cool stuff. + ''' + Do cool stuff. - >>> x = """tricksy""" - ''' - pass + >>> 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. + ''' + Do cool stuff. - >>> x = f"""tricksy""" - ''' - pass + >>> x = f"""tricksy""" + ''' + pass # Tests nested doctests are ignored. That is, we don't format doctests @@ -5292,30 +9670,30 @@ def doctest_with_triple_inverted_fstring(): # 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. + ''' + Do cool stuff. - >>> def nested(x): - ... """ - ... Do nested cool stuff. - ... >>> func_call( 5 ) - ... """ - ... pass - ''' - pass + >>> 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. + """ + Do cool stuff. - >>> assert "Easy!" - >>> import math - >>> math.floor(1.9) - 1 - """ - pass + >>> assert "Easy!" + >>> import math + >>> math.floor(1.9) + 1 + """ + pass # Tests that long lines get wrapped... appropriately. @@ -5331,61 +9709,61 @@ def doctest_varying_start_column(): # would be to expose another formatter option for controlling the # line-width of code snippets independently. def doctest_long_lines(): - """ - Do cool stuff. + """ + 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) + 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 - ) + 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. + """ + Do cool stuff. - >>> cool-stuff( x ): - 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool-stuff( x ): - ... print( f"hi {x}" ); - 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 - """ - pass + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 + """ + pass # Checks that a doctest with some proper indentation and some improper @@ -5393,34 +9771,34 @@ def doctest_skipped_inconsistent_indent(): # 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. + """ + Do cool stuff. - >>> def cool_stuff(x): - ... print(x) - ... print( f"hi {x}" ); - hi 2 - """ - pass + >>> 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. + """ + Do cool stuff. - >>> foo( x ) - ... '''tri'''cksy''' - """ - pass + >>> foo( x ) + ... '''tri'''cksy''' + """ + pass # Tests that a doctest on a single line is skipped. def doctest_skipped_one_line(): - ">>> foo( x )" - pass + ">>> foo( x )" + pass # f-strings are not considered docstrings[1], so any doctests @@ -5428,25 +9806,25 @@ def doctest_skipped_one_line(): # # [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals def doctest_skipped_fstring(): - f""" + f""" Do cool stuff. >>> cool_stuff( 1 ) 2 """ - pass + 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. + """ + Do cool stuff. - >>> x = '\"\"\"' - """ - pass + >>> x = '\"\"\"' + """ + pass ############################################################################### @@ -5464,128 +9842,128 @@ def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): def rst_literal_simple(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass def rst_literal_simple_continued(): - """ - Do cool stuff:: - - def cool_stuff(x): - print(f"hi {x}") + """ + Do cool stuff:: - Done. - """ - pass + def cool_stuff(x): + print(f"hi {x}") + + Done. + """ + pass # Tests that we can end the literal block on the second # to last line of the docstring. def rst_literal_second_to_last(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) - """ - pass + cool_stuff(1) + """ + pass # Tests that we can end the literal block on the actual # last line of the docstring. def rst_literal_actually_last(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1)""" - pass + cool_stuff(1)""" + pass def rst_literal_with_blank_lines(): - """ - Do cool stuff:: + """ + Do cool stuff:: - def cool_stuff(x): - print(f"hi {x}") + def cool_stuff(x): + print(f"hi {x}") - def other_stuff(y): - print(y) + def other_stuff(y): + print(y) - Done. - """ - pass + Done. + """ + pass # Extra blanks should be preserved. def rst_literal_extra_blanks(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass # If a literal block is never properly ended (via a non-empty unindented line), # then the end of the block should be the last non-empty line. And subsequent # empty lines should be preserved as-is. def rst_literal_extra_blanks_at_end(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) + cool_stuff(1) - """ - pass + """ + pass # A literal block can contain many empty lines and it should not end the block # if it continues. def rst_literal_extra_blanks_in_snippet(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) + cool_stuff(1) - cool_stuff(2) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # This tests that a unindented line appearing after an indented line (but where # the indent is still beyond the minimum) gets formatted properly. def rst_literal_subsequent_line_not_indented(): - """ - Do cool stuff:: + """ + Do cool stuff:: - if True: - cool_stuff( - ''' - hiya''' - ) + if True: + cool_stuff( + ''' + hiya''' + ) - Done. - """ - pass + Done. + """ + pass # This checks that if the first line in a code snippet has been indented with @@ -5597,27 +9975,27 @@ def rst_literal_subsequent_line_not_indented(): # itself also seems to recognize this as a code block, although it appears # under-specified.) def rst_literal_first_line_indent_uses_tabs_4spaces(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass # Like the test above, but with multiple lines. def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) - cool_stuff(2) + cool_stuff(1) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # Another test with tabs, except in this case, if your tabwidth is less than @@ -5628,42 +10006,42 @@ def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): # snippet is actually seen as being more indented than the opening `::` line. # As with the above example, reST seems to behave the same way here. def rst_literal_first_line_indent_uses_tabs_8spaces(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass # Like the test above, but with multiple lines. def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) - cool_stuff(2) + cool_stuff(1) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # Tests that if two lines in a literal block are indented to the same level # but by different means (tabs versus spaces), then we correctly recognize the # block and format it. def rst_literal_first_line_tab_second_line_spaces(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) - cool_stuff(2) + cool_stuff(1) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # Tests that when two lines in a code snippet have weird and inconsistent @@ -5685,122 +10063,122 @@ def rst_literal_first_line_tab_second_line_spaces(): # but we might want to switch to the alternative if cases like this show up in # the real world. ---AG def rst_literal_odd_indentation(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff(1) - cool_stuff(2) + cool_stuff(1) + cool_stuff(2) - Done. - """ - pass + Done. + """ + pass # Tests that having a line with a lone `::` works as an introduction of a # literal block. def rst_literal_lone_colon(): - """ - Do cool stuff. + """ + Do cool stuff. - :: + :: - cool_stuff(1) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass def rst_directive_simple(): - """ - .. code-block:: python + """ + .. code-block:: python - cool_stuff(1) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass def rst_directive_case_insensitive(): - """ - .. cOdE-bLoCk:: python + """ + .. cOdE-bLoCk:: python - cool_stuff(1) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass def rst_directive_sourcecode(): - """ - .. sourcecode:: python + """ + .. sourcecode:: python - cool_stuff(1) + cool_stuff(1) - Done. - """ - pass + Done. + """ + pass def rst_directive_options(): - """ - .. code-block:: python - :linenos: - :emphasize-lines: 2,3 - :name: blah blah + """ + .. code-block:: python + :linenos: + :emphasize-lines: 2,3 + :name: blah blah - cool_stuff(1) - cool_stuff(2) - cool_stuff(3) - cool_stuff(4) + cool_stuff(1) + cool_stuff(2) + cool_stuff(3) + cool_stuff(4) - Done. - """ - pass + Done. + """ + pass # In this case, since `pycon` isn't recognized as a Python code snippet, the # docstring reformatter ignores it. But it then picks up the doctest and # reformats it. def rst_directive_doctest(): - """ - .. code-block:: pycon + """ + .. code-block:: pycon - >>> cool_stuff(1) + >>> cool_stuff(1) - Done. - """ - pass + Done. + """ + pass # This checks that if the first non-empty line after the start of a literal # block is not indented more than the line containing the `::`, then it is not # treated as a code snippet. def rst_literal_skipped_first_line_not_indented(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # Like the test above, but inserts an indented line after the un-indented one. # This should not cause the literal block to be resumed. def rst_literal_skipped_first_line_not_indented_then_indented(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) + cool_stuff( 1 ) + cool_stuff( 2 ) - Done. - """ - pass + Done. + """ + pass # This also checks that a code snippet is not reformatted when the indentation @@ -5809,27 +10187,27 @@ def rst_literal_skipped_first_line_not_indented_then_indented(): # length is computed by assuming a tabwidth equal to 8. reST also rejects this # and doesn't treat it as a literal block. def rst_literal_skipped_first_line_not_indented_tab(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # Like the previous test, but adds a second line. def rst_literal_skipped_first_line_not_indented_tab_multiple(): - """ - Do cool stuff:: + """ + Do cool stuff:: - cool_stuff( 1 ) - cool_stuff( 2 ) + cool_stuff( 1 ) + cool_stuff( 2 ) - Done. - """ - pass + Done. + """ + pass # Tests that a code block with a second line that is not properly indented gets @@ -5838,16 +10216,16 @@ def rst_literal_skipped_first_line_not_indented_tab_multiple(): # One trick here is that we need to make sure the Python code in the snippet is # valid, otherwise it would be skipped because of invalid Python. def rst_literal_skipped_subsequent_line_not_indented(): - """ - Do cool stuff:: + """ + Do cool stuff:: - if True: - cool_stuff( ''' - hiya''' ) + if True: + cool_stuff( ''' + hiya''' ) - Done. - """ - pass + Done. + """ + pass # In this test, we write what looks like a code-block, but it should be treated @@ -5857,14 +10235,14 @@ def rst_literal_skipped_subsequent_line_not_indented(): # rules, but we currently consider the `.. ` prefix to indicate that it is not # a literal block. def rst_literal_skipped_not_directive(): - """ - .. code-block:: + """ + .. code-block:: - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # In this test, we start a line with `.. `, which makes it look like it might @@ -5875,15 +10253,15 @@ def rst_literal_skipped_not_directive(): # code snippet. The reST render I was using to test things does actually treat # this as a code block, so we may be out of conformance here. def rst_literal_skipped_possible_false_negative(): - """ - This is a test. - .. This is a test:: + """ + This is a test. + .. This is a test:: - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # This tests that a doctest inside of a reST literal block doesn't get @@ -5891,1749 +10269,1893 @@ def rst_literal_skipped_possible_false_negative(): # like it might be the right behavior since it is a literal block. (The doctest # makes the Python code invalid.) def rst_literal_skipped_doctest(): - """ - Do cool stuff:: + """ + Do cool stuff:: - >>> cool_stuff( 1 ) + >>> cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass + + +def rst_literal_skipped_markdown(): + """ + Do cool stuff:: + + ```py + cool_stuff( 1 ) + ``` + + Done. + """ + pass def rst_directive_skipped_not_indented(): - """ - .. code-block:: python + """ + .. code-block:: python - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass def rst_directive_skipped_wrong_language(): - """ - .. code-block:: rust + """ + .. code-block:: rust - cool_stuff( 1 ) + cool_stuff( 1 ) - Done. - """ - pass + Done. + """ + pass # This gets skipped for the same reason that the doctest in a literal block # gets skipped. def rst_directive_skipped_doctest(): - """ - .. code-block:: python - - >>> cool_stuff( 1 ) + """ + .. code-block:: python - Done. - """ - pass -``` + >>> cool_stuff( 1 ) + Done. + """ + 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 ############################################################################### -# DOCTEST CODE EXAMPLES +# Markdown CODE EXAMPLES # # This section shows examples of docstrings that contain code snippets in -# Python's "doctest" format. +# Markdown fenced code blocks. # -# See: https://docs.python.org/3/library/doctest.html +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks ############################################################################### -# The simplest doctest to ensure basic formatting works. -def doctest_simple(): + +def markdown_simple(): """ Do cool stuff. - >>> cool_stuff(1) - 2 + ```py + cool_stuff(1) + ``` + + Done. """ pass -# Another simple test, but one where the Python code -# extends over multiple lines. -def doctest_simple_continued(): +def markdown_simple_continued(): """ Do cool stuff. - >>> def cool_stuff(x): - ... print(f"hi {x}") - hi 2 + ```python + def cool_stuff(x): + print(f"hi {x}") + ``` + + Done. """ pass -# Test that we support multiple directly adjacent -# doctests. -def doctest_adjacent(): +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): """ Do cool stuff. - >>> cool_stuff(x) - >>> cool_stuff(y) - 2 + ``` + cool_stuff(1) + ``` + + Done. """ pass -# Test that a doctest on the last non-whitespace line of a docstring -# reformats correctly. -def doctest_last_line(): +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): """ Do cool stuff. - >>> cool_stuff(x) + ~~~py + cool_stuff(1) + ~~~ + + Done. """ pass -# Test that a doctest that continues to the last non-whitespace line of -# a docstring reformats correctly. -def doctest_last_line_continued(): +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): """ Do cool stuff. - >>> def cool_stuff(x): - ... print(f"hi {x}") + ```py + cool_stuff(1) + `````` + + Done. """ pass -# Test that a doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line(): +# Tests that an invalid closing fence is treated as invalid. +# +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): """ Do cool stuff. - >>> cool_stuff(x)""" + ```py + cool_stuff(1) + ''' + ```invalid + ''' + cool_stuff(2) + ``` + + Done. + """ pass -# Test that a continued doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line_continued(): +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): """ Do cool stuff. - >>> cool_stuff(x) - ... more(y)""" + `````` + do_something( + ''' + ``` + did i trick you? + ``` + ''' + ) + `````` + + Done. + """ pass -# Test that a doctest is correctly identified and formatted with a blank -# continuation line. -def doctest_blank_continued(): +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): """ Do cool stuff. - >>> def cool_stuff(x): - ... print(x) - ... - ... print(x) - """ + ```py + cool_stuff(1)""" 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(): +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): """ Do cool stuff. - >>> def cool_stuff(x): - ... print(x) - ... print(x) + ```py + cool_stuff(1) + ``` """ 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(): +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): """ Do cool stuff. - >>> def cool_stuff(x): - ... print(x) - ... print(x) + ```py + cool_stuff(1)""" + pass + - And say something else. +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): """ + Do cool stuff. + + ```py + cool_stuff(1) + ```""" 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(): +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): """ Do cool stuff. - >>> x = '''tricksy''' - """ + ```py + cool_stuff(1)""" 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(): +def markdown_with_blank_lines(): """ Do cool stuff. - >>> x = f'''tricksy''' + ```py + def cool_stuff(x): + print(f"hi {x}") + + + def other_stuff(y): + print(y) + ``` + + Done. """ 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(): +def markdown_first_line_indent_uses_tabs_4spaces(): """ Do cool stuff. - >>> x = '''\"\"\"''' + ```py + cool_stuff(1) + ``` + + Done. """ pass -# Tests that inverting the triple quoting works as expected. -def doctest_with_triple_inverted(): - ''' +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): + """ Do cool stuff. - >>> x = """tricksy""" - ''' + ```py + cool_stuff(1) + cool_stuff(2) + ``` + + Done. + """ pass -# Tests that inverting the triple quoting with an f-string works as -# expected. -def doctest_with_triple_inverted_fstring(): - ''' +def markdown_first_line_indent_uses_tabs_8spaces(): + """ Do cool stuff. - >>> x = f"""tricksy""" - ''' + ```py + cool_stuff(1) + ``` + + Done. + """ 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(): - ''' +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): + """ Do cool stuff. - >>> def nested(x): - ... """ - ... Do nested cool stuff. - ... >>> func_call( 5 ) - ... """ - ... pass - ''' + ```py + cool_stuff(1) + cool_stuff(2) + ``` + + Done. + """ pass -# Tests that the starting column does not matter. -def doctest_varying_start_column(): +def markdown_first_line_tab_second_line_spaces(): """ Do cool stuff. - >>> assert "Easy!" - >>> import math - >>> math.floor(1.9) - 1 + ```py + cool_stuff(1) + cool_stuff(2) + ``` + + Done. """ 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(): +def markdown_odd_indentation(): """ 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) + ```py + cool_stuff(1) + cool_stuff(2) + ``` - 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 - ... ) + Done. """ - # 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 - ) + pass -# Checks that a simple but invalid doctest gets skipped. -def doctest_skipped_simple(): +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): """ Do cool stuff. - >>> cool-stuff( x ): - 2 + ```py + cool_stuff(1) + ``` + + Done. """ pass -# Checks that a simple doctest that is continued over multiple lines, -# but is invalid, gets skipped. -def doctest_skipped_simple_continued(): +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): """ Do cool stuff. - >>> def cool-stuff( x ): - ... print( f"hi {x}" ); - 2 + ```py + cool_stuff(1) + + + cool_stuff(2) + ``` + + Done. """ pass -# Checks that a doctest with improper indentation gets skipped. -def doctest_skipped_inconsistent_indent(): +def markdown_weird_closing(): """ - Do cool stuff. + Code block with weirdly placed closing fences. - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 + ```python + cool_stuff(1) + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` + + Now the code block is closed """ 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(): +def markdown_over_indented(): """ - Do cool stuff. - - >>> def cool_stuff(x): - ... print(x) - ... print( f"hi {x}" ); - hi 2 + A docstring + over intended + ```python + print(5) + ``` """ 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(): +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): """ Do cool stuff. - >>> foo( x ) - ... '''tri'''cksy''' + ```py + cool_stuff( 1 ) + + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. """ pass -# Tests that a doctest on a single line is skipped. -def doctest_skipped_one_line(): - ">>> foo( x )" +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): + """ + Do cool stuff. + + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` + + Done. + """ pass -# f-strings are not considered docstrings[1], so any doctests -# inside of them should not be formatted. +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. # -# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals -def doctest_skipped_fstring(): - f""" - Do cool stuff. +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): + """ + Do cool stuff. - >>> cool_stuff( 1 ) - 2 - """ + ```py + cool_stuff( 1 ) + ``` + + Done. + """ 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(): +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): """ Do cool stuff. - >>> x = '\"\"\"' + ```py + cool_stuff( 1 ) + ``` + + Done. """ pass -############################################################################### -# reStructuredText CODE EXAMPLES -# -# This section shows examples of docstrings that contain code snippets in -# reStructuredText formatted code blocks. +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. # -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 -############################################################################### - - -def rst_literal_simple(): +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff(1) + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` Done. """ pass -def rst_literal_simple_continued(): +def markdown_skipped_doctest(): """ - Do cool stuff:: + Do cool stuff. - def cool_stuff(x): - print(f"hi {x}") + ```py + >>> cool_stuff( 1 ) + ``` Done. """ pass -# Tests that we can end the literal block on the second -# to last line of the docstring. -def rst_literal_second_to_last(): +def markdown_skipped_rst_literal(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff(1) + ```py + And do this:: + + cool_stuff( 1 ) + + ``` + + Done. """ pass -# Tests that we can end the literal block on the actual -# last line of the docstring. -def rst_literal_actually_last(): +def markdown_skipped_rst_directive(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff(1)""" - pass + ```py + .. code-block:: python + + cool_stuff( 1 ) + ``` -def rst_literal_with_blank_lines(): + Done. """ - Do cool stuff:: + pass +``` - def cool_stuff(x): - print(f"hi {x}") +### 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 +``` - def other_stuff(y): - print(y) +```python +############################################################################### +# DOCTEST CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Python's "doctest" format. +# +# See: https://docs.python.org/3/library/doctest.html +############################################################################### - Done. +# The simplest doctest to ensure basic formatting works. +def doctest_simple(): """ - pass - + Do cool stuff. -# Extra blanks should be preserved. -def rst_literal_extra_blanks(): + >>> cool_stuff(1) + 2 """ - Do cool stuff:: - - - - cool_stuff(1) + pass +# Another simple test, but one where the Python code +# extends over multiple lines. +def doctest_simple_continued(): + """ + Do cool stuff. - Done. + >>> def cool_stuff(x): + ... print(f"hi {x}") + hi 2 """ pass -# If a literal block is never properly ended (via a non-empty unindented line), -# then the end of the block should be the last non-empty line. And subsequent -# empty lines should be preserved as-is. -def rst_literal_extra_blanks_at_end(): +# Test that we support multiple directly adjacent +# doctests. +def doctest_adjacent(): """ - Do cool stuff:: - - - cool_stuff(1) - - + Do cool stuff. + >>> cool_stuff(x) + >>> cool_stuff(y) + 2 """ pass -# A literal block can contain many empty lines and it should not end the block -# if it continues. -def rst_literal_extra_blanks_in_snippet(): +# Test that a doctest on the last non-whitespace line of a docstring +# reformats correctly. +def doctest_last_line(): """ - Do cool stuff:: - - cool_stuff(1) - - - cool_stuff(2) + Do cool stuff. - Done. + >>> cool_stuff(x) """ pass -# This tests that a unindented line appearing after an indented line (but where -# the indent is still beyond the minimum) gets formatted properly. -def rst_literal_subsequent_line_not_indented(): +# 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:: - - if True: - cool_stuff( - ''' - hiya''' - ) + Do cool stuff. - Done. + >>> def cool_stuff(x): + ... print(f"hi {x}") """ pass -# This checks that if the first line in a code snippet has been indented with -# tabs, then so long as its "indentation length" is considered bigger than the -# line with `::`, it is reformatted as code. -# -# (If your tabwidth is set to 4, then it looks like the code snippet -# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST -# itself also seems to recognize this as a code block, although it appears -# under-specified.) -def rst_literal_first_line_indent_uses_tabs_4spaces(): +# Test that a doctest on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line(): """ - Do cool stuff:: - - cool_stuff(1) + Do cool stuff. - Done. - """ + >>> cool_stuff(x)""" pass -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): +# Test that a continued doctest on the real last line of a docstring reformats +# correctly. +def doctest_really_last_line_continued(): """ - Do cool stuff:: - - cool_stuff(1) - cool_stuff(2) + Do cool stuff. - Done. - """ + >>> cool_stuff(x) + ... more(y)""" pass -# Another test with tabs, except in this case, if your tabwidth is less than -# 8, than the code snippet actually looks like its indent is *less* than the -# opening line with a `::`. One might presume this means that the code snippet -# is not treated as a literal block and thus not reformatted, but since we -# assume all tabs have tabwidth=8 when computing indentation length, the code -# snippet is actually seen as being more indented than the opening `::` line. -# As with the above example, reST seems to behave the same way here. -def rst_literal_first_line_indent_uses_tabs_8spaces(): +# Test that a doctest is correctly identified and formatted with a blank +# continuation line. +def doctest_blank_continued(): """ - Do cool stuff:: - - cool_stuff(1) + Do cool stuff. - Done. + >>> def cool_stuff(x): + ... print(x) + ... + ... print(x) """ pass -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): +# 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:: - - cool_stuff(1) - cool_stuff(2) + Do cool stuff. - Done. + >>> def cool_stuff(x): + ... print(x) + ... print(x) """ pass -# Tests that if two lines in a literal block are indented to the same level -# but by different means (tabs versus spaces), then we correctly recognize the -# block and format it. -def rst_literal_first_line_tab_second_line_spaces(): +# 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:: + Do cool stuff. - cool_stuff(1) - cool_stuff(2) + >>> def cool_stuff(x): + ... print(x) + ... print(x) - Done. + And say something else. """ pass -# Tests that when two lines in a code snippet have weird and inconsistent -# indentation, the code still gets formatted so long as the indent is greater -# than the indent of the `::` line. -# -# In this case, the minimum indent is 5 spaces (from the second line) where as -# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). -# The minimum indent is stripped from each code line. Since tabs aren't -# divisible, the entire tab is stripped, which means the first and second lines -# wind up with the same level of indentation. -# -# An alternative behavior here would be that the tab is replaced with 3 spaces -# instead of being stripped entirely. The code snippet itself would then have -# inconsistent indentation to the point of being invalid Python, and thus code -# formatting would be skipped. -# -# I decided on the former behavior because it seems a bit easier to implement, -# but we might want to switch to the alternative if cases like this show up in -# the real world. ---AG -def rst_literal_odd_indentation(): +# 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:: - - cool_stuff(1) - cool_stuff(2) + Do cool stuff. - Done. + >>> x = '''tricksy''' """ pass -# Tests that having a line with a lone `::` works as an introduction of a -# literal block. -def rst_literal_lone_colon(): +# 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. - :: - - cool_stuff(1) - - Done. + >>> x = f'''tricksy''' """ pass -def rst_directive_simple(): +# Another nested multi-line string case, but with triple escaped double +# quotes inside a triple single quoted string. +def doctest_with_triple_escaped_double(): """ - .. code-block:: python - - cool_stuff(1) + Do cool stuff. - Done. + >>> x = '''\"\"\"''' """ pass -def rst_directive_case_insensitive(): - """ - .. cOdE-bLoCk:: python - - cool_stuff(1) +# Tests that inverting the triple quoting works as expected. +def doctest_with_triple_inverted(): + ''' + Do cool stuff. - Done. - """ + >>> x = """tricksy""" + ''' pass -def rst_directive_sourcecode(): - """ - .. sourcecode:: python - - cool_stuff(1) +# Tests that inverting the triple quoting with an f-string works as +# expected. +def doctest_with_triple_inverted_fstring(): + ''' + Do cool stuff. - Done. - """ + >>> x = f"""tricksy""" + ''' pass -def rst_directive_options(): - """ - .. code-block:: python - :linenos: - :emphasize-lines: 2,3 - :name: blah blah - - cool_stuff(1) - cool_stuff(2) - cool_stuff(3) - cool_stuff(4) +# 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. - Done. - """ + >>> def nested(x): + ... """ + ... Do nested cool stuff. + ... >>> func_call( 5 ) + ... """ + ... pass + ''' pass -# In this case, since `pycon` isn't recognized as a Python code snippet, the -# docstring reformatter ignores it. But it then picks up the doctest and -# reformats it. -def rst_directive_doctest(): +# Tests that the starting column does not matter. +def doctest_varying_start_column(): """ - .. code-block:: pycon - - >>> cool_stuff(1) + Do cool stuff. - Done. + >>> assert "Easy!" + >>> import math + >>> math.floor(1.9) + 1 """ pass -# This checks that if the first non-empty line after the start of a literal -# block is not indented more than the line containing the `::`, then it is not -# treated as a code snippet. -def rst_literal_skipped_first_line_not_indented(): +# 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:: + Do cool stuff. - cool_stuff( 1 ) + 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) - Done. + 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 + ... ) """ - pass + # 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 + ) -# Like the test above, but inserts an indented line after the un-indented one. -# This should not cause the literal block to be resumed. -def rst_literal_skipped_first_line_not_indented_then_indented(): +# Checks that a simple but invalid doctest gets skipped. +def doctest_skipped_simple(): """ - Do cool stuff:: - - cool_stuff( 1 ) - cool_stuff( 2 ) + Do cool stuff. - Done. + >>> cool-stuff( x ): + 2 """ pass -# This also checks that a code snippet is not reformatted when the indentation -# of the first line is not more than the line with `::`, but this uses tabs to -# make it a little more confounding. It relies on the fact that indentation -# length is computed by assuming a tabwidth equal to 8. reST also rejects this -# and doesn't treat it as a literal block. -def rst_literal_skipped_first_line_not_indented_tab(): +# Checks that a simple doctest that is continued over multiple lines, +# but is invalid, gets skipped. +def doctest_skipped_simple_continued(): """ - Do cool stuff:: - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> def cool-stuff( x ): + ... print( f"hi {x}" ); + 2 """ pass -# Like the previous test, but adds a second line. -def rst_literal_skipped_first_line_not_indented_tab_multiple(): +# Checks that a doctest with improper indentation gets skipped. +def doctest_skipped_inconsistent_indent(): """ - Do cool stuff:: - - cool_stuff( 1 ) - cool_stuff( 2 ) + Do cool stuff. - Done. + >>> def cool_stuff( x ): + ... print( f"hi {x}" ); + hi 2 """ pass -# Tests that a code block with a second line that is not properly indented gets -# skipped. A valid code block needs to have an empty line separating these. -# -# One trick here is that we need to make sure the Python code in the snippet is -# valid, otherwise it would be skipped because of invalid Python. -def rst_literal_skipped_subsequent_line_not_indented(): +# 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:: - - if True: - cool_stuff( ''' - hiya''' ) + Do cool stuff. - Done. + >>> def cool_stuff(x): + ... print(x) + ... print( f"hi {x}" ); + hi 2 """ pass -# In this test, we write what looks like a code-block, but it should be treated -# as invalid due to the missing `language` argument. -# -# It does still look like it could be a literal block according to the literal -# rules, but we currently consider the `.. ` prefix to indicate that it is not -# a literal block. -def rst_literal_skipped_not_directive(): +# 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(): """ - .. code-block:: - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> foo( x ) + ... '''tri'''cksy''' """ pass -# In this test, we start a line with `.. `, which makes it look like it might -# be a directive. But instead continue it as if it was just some periods from -# the previous line, and then try to end it by starting a literal block. -# -# But because of the `.. ` in the beginning, we wind up not treating this as a -# code snippet. The reST render I was using to test things does actually treat -# this as a code block, so we may be out of conformance here. -def rst_literal_skipped_possible_false_negative(): - """ - This is a test. - .. This is a test:: - - cool_stuff( 1 ) - - Done. - """ +# Tests that a doctest on a single line is skipped. +def doctest_skipped_one_line(): + ">>> foo( x )" pass -# This tests that a doctest inside of a reST literal block doesn't get -# reformatted. It's plausible this isn't the right behavior, but it also seems -# like it might be the right behavior since it is a literal block. (The doctest -# makes the Python code invalid.) -def rst_literal_skipped_doctest(): - """ - Do cool stuff:: - - >>> cool_stuff( 1 ) +# 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. - Done. - """ + >>> cool_stuff( 1 ) + 2 + """ pass -def rst_directive_skipped_not_indented(): +# 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(): """ - .. code-block:: python - - cool_stuff( 1 ) + Do cool stuff. - Done. + >>> x = '\"\"\"' """ pass -def rst_directive_skipped_wrong_language(): +############################################################################### +# reStructuredText CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# reStructuredText formatted code blocks. +# +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks +# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 +# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 +############################################################################### + + +def rst_literal_simple(): """ - .. code-block:: rust + Do cool stuff:: - cool_stuff( 1 ) + cool_stuff(1) Done. """ pass -# This gets skipped for the same reason that the doctest in a literal block -# gets skipped. -def rst_directive_skipped_doctest(): +def rst_literal_simple_continued(): """ - .. code-block:: python + Do cool stuff:: - >>> cool_stuff( 1 ) + def cool_stuff(x): + print(f"hi {x}") Done. """ 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 -############################################################################### -# DOCTEST CODE EXAMPLES -# -# This section shows examples of docstrings that contain code snippets in -# Python's "doctest" format. -# -# See: https://docs.python.org/3/library/doctest.html -############################################################################### - -# The simplest doctest to ensure basic formatting works. -def doctest_simple(): +# Tests that we can end the literal block on the second +# to last line of the docstring. +def rst_literal_second_to_last(): """ - Do cool stuff. + Do cool stuff:: - >>> cool_stuff(1) - 2 + cool_stuff(1) """ pass -# Another simple test, but one where the Python code -# extends over multiple lines. -def doctest_simple_continued(): +# Tests that we can end the literal block on the actual +# last line of the docstring. +def rst_literal_actually_last(): """ - Do cool stuff. + Do cool stuff:: - >>> def cool_stuff(x): - ... print(f"hi {x}") - hi 2 - """ + cool_stuff(1)""" pass -# Test that we support multiple directly adjacent -# doctests. -def doctest_adjacent(): +def rst_literal_with_blank_lines(): """ - Do cool stuff. + Do cool stuff:: - >>> cool_stuff(x) - >>> cool_stuff(y) - 2 + def cool_stuff(x): + print(f"hi {x}") + + + def other_stuff(y): + print(y) + + Done. """ pass -# Test that a doctest on the last non-whitespace line of a docstring -# reformats correctly. -def doctest_last_line(): +# Extra blanks should be preserved. +def rst_literal_extra_blanks(): """ - Do cool stuff. + Do cool stuff:: - >>> cool_stuff(x) + + + cool_stuff(1) + + + + Done. """ pass -# Test that a doctest that continues to the last non-whitespace line of -# a docstring reformats correctly. -def doctest_last_line_continued(): +# If a literal block is never properly ended (via a non-empty unindented line), +# then the end of the block should be the last non-empty line. And subsequent +# empty lines should be preserved as-is. +def rst_literal_extra_blanks_at_end(): """ - Do cool stuff. + Do cool stuff:: + + + cool_stuff(1) + + - >>> def cool_stuff(x): - ... print(f"hi {x}") """ pass -# Test that a doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line(): +# A literal block can contain many empty lines and it should not end the block +# if it continues. +def rst_literal_extra_blanks_in_snippet(): """ - Do cool stuff. + Do cool stuff:: - >>> cool_stuff(x)""" - pass + cool_stuff(1) -# Test that a continued doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line_continued(): - """ - Do cool stuff. + cool_stuff(2) - >>> cool_stuff(x) - ... more(y)""" + Done. + """ pass -# Test that a doctest is correctly identified and formatted with a blank -# continuation line. -def doctest_blank_continued(): +# This tests that a unindented line appearing after an indented line (but where +# the indent is still beyond the minimum) gets formatted properly. +def rst_literal_subsequent_line_not_indented(): """ - Do cool stuff. + Do cool stuff:: - >>> def cool_stuff(x): - ... print(x) - ... - ... print(x) + if True: + cool_stuff( + ''' + hiya''' + ) + + Done. """ 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(): +# This checks that if the first line in a code snippet has been indented with +# tabs, then so long as its "indentation length" is considered bigger than the +# line with `::`, it is reformatted as code. +# +# (If your tabwidth is set to 4, then it looks like the code snippet +# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST +# itself also seems to recognize this as a code block, although it appears +# under-specified.) +def rst_literal_first_line_indent_uses_tabs_4spaces(): """ - Do cool stuff. + Do cool stuff:: - >>> def cool_stuff(x): - ... print(x) - ... print(x) + cool_stuff(1) + + Done. """ 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(): +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): """ - Do cool stuff. + Do cool stuff:: - >>> def cool_stuff(x): - ... print(x) - ... print(x) + cool_stuff(1) + cool_stuff(2) - And say something else. + Done. """ 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(): +# Another test with tabs, except in this case, if your tabwidth is less than +# 8, than the code snippet actually looks like its indent is *less* than the +# opening line with a `::`. One might presume this means that the code snippet +# is not treated as a literal block and thus not reformatted, but since we +# assume all tabs have tabwidth=8 when computing indentation length, the code +# snippet is actually seen as being more indented than the opening `::` line. +# As with the above example, reST seems to behave the same way here. +def rst_literal_first_line_indent_uses_tabs_8spaces(): """ - Do cool stuff. + Do cool stuff:: - >>> x = '''tricksy''' + cool_stuff(1) + + Done. """ 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(): +# Like the test above, but with multiple lines. +def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): """ - Do cool stuff. + Do cool stuff:: - >>> x = f'''tricksy''' + cool_stuff(1) + cool_stuff(2) + + Done. """ 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(): +# Tests that if two lines in a literal block are indented to the same level +# but by different means (tabs versus spaces), then we correctly recognize the +# block and format it. +def rst_literal_first_line_tab_second_line_spaces(): """ - Do cool stuff. + Do cool stuff:: - >>> x = '''\"\"\"''' + cool_stuff(1) + cool_stuff(2) + + Done. """ pass -# Tests that inverting the triple quoting works as expected. -def doctest_with_triple_inverted(): - ''' - Do cool stuff. +# Tests that when two lines in a code snippet have weird and inconsistent +# indentation, the code still gets formatted so long as the indent is greater +# than the indent of the `::` line. +# +# In this case, the minimum indent is 5 spaces (from the second line) where as +# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). +# The minimum indent is stripped from each code line. Since tabs aren't +# divisible, the entire tab is stripped, which means the first and second lines +# wind up with the same level of indentation. +# +# An alternative behavior here would be that the tab is replaced with 3 spaces +# instead of being stripped entirely. The code snippet itself would then have +# inconsistent indentation to the point of being invalid Python, and thus code +# formatting would be skipped. +# +# I decided on the former behavior because it seems a bit easier to implement, +# but we might want to switch to the alternative if cases like this show up in +# the real world. ---AG +def rst_literal_odd_indentation(): + """ + Do cool stuff:: - >>> x = """tricksy""" - ''' + cool_stuff(1) + cool_stuff(2) + + Done. + """ pass -# Tests that inverting the triple quoting with an f-string works as -# expected. -def doctest_with_triple_inverted_fstring(): - ''' +# Tests that having a line with a lone `::` works as an introduction of a +# literal block. +def rst_literal_lone_colon(): + """ Do cool stuff. - >>> x = f"""tricksy""" - ''' + :: + + cool_stuff(1) + + Done. + """ 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 rst_directive_simple(): + """ + .. code-block:: python - >>> def nested(x): - ... """ - ... Do nested cool stuff. - ... >>> func_call( 5 ) - ... """ - ... pass - ''' + cool_stuff(1) + + Done. + """ pass -# Tests that the starting column does not matter. -def doctest_varying_start_column(): +def rst_directive_case_insensitive(): """ - Do cool stuff. + .. cOdE-bLoCk:: python + + cool_stuff(1) - >>> assert "Easy!" - >>> import math - >>> math.floor(1.9) - 1 + Done. """ 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(): +def rst_directive_sourcecode(): """ - Do cool stuff. + .. sourcecode:: python - 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) + cool_stuff(1) - 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 - ... ) + Done. """ - # 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 - ) + pass -# Checks that a simple but invalid doctest gets skipped. -def doctest_skipped_simple(): +def rst_directive_options(): """ - Do cool stuff. + .. code-block:: python + :linenos: + :emphasize-lines: 2,3 + :name: blah blah - >>> cool-stuff( x ): - 2 + cool_stuff(1) + cool_stuff(2) + cool_stuff(3) + cool_stuff(4) + + Done. """ pass -# Checks that a simple doctest that is continued over multiple lines, -# but is invalid, gets skipped. -def doctest_skipped_simple_continued(): +# In this case, since `pycon` isn't recognized as a Python code snippet, the +# docstring reformatter ignores it. But it then picks up the doctest and +# reformats it. +def rst_directive_doctest(): """ - Do cool stuff. + .. code-block:: pycon - >>> def cool-stuff( x ): - ... print( f"hi {x}" ); - 2 + >>> cool_stuff(1) + + Done. """ pass -# Checks that a doctest with improper indentation gets skipped. -def doctest_skipped_inconsistent_indent(): +# This checks that if the first non-empty line after the start of a literal +# block is not indented more than the line containing the `::`, then it is not +# treated as a code snippet. +def rst_literal_skipped_first_line_not_indented(): """ - Do cool stuff. + Do cool stuff:: - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 + cool_stuff( 1 ) + + Done. """ 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(): +# Like the test above, but inserts an indented line after the un-indented one. +# This should not cause the literal block to be resumed. +def rst_literal_skipped_first_line_not_indented_then_indented(): """ - Do cool stuff. + Do cool stuff:: - >>> def cool_stuff(x): - ... print(x) - ... print( f"hi {x}" ); - hi 2 + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. """ 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(): +# This also checks that a code snippet is not reformatted when the indentation +# of the first line is not more than the line with `::`, but this uses tabs to +# make it a little more confounding. It relies on the fact that indentation +# length is computed by assuming a tabwidth equal to 8. reST also rejects this +# and doesn't treat it as a literal block. +def rst_literal_skipped_first_line_not_indented_tab(): """ - Do cool stuff. + Do cool stuff:: - >>> foo( x ) - ... '''tri'''cksy''' + cool_stuff( 1 ) + + Done. """ pass -# Tests that a doctest on a single line is skipped. -def doctest_skipped_one_line(): - ">>> foo( x )" +# Like the previous test, but adds a second line. +def rst_literal_skipped_first_line_not_indented_tab_multiple(): + """ + Do cool stuff:: + + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. + """ pass -# f-strings are not considered docstrings[1], so any doctests -# inside of them should not be formatted. +# Tests that a code block with a second line that is not properly indented gets +# skipped. A valid code block needs to have an empty line separating these. # -# [1]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals -def doctest_skipped_fstring(): - f""" - Do cool stuff. +# One trick here is that we need to make sure the Python code in the snippet is +# valid, otherwise it would be skipped because of invalid Python. +def rst_literal_skipped_subsequent_line_not_indented(): + """ + Do cool stuff:: - >>> cool_stuff( 1 ) - 2 - """ + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ 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(): +# In this test, we write what looks like a code-block, but it should be treated +# as invalid due to the missing `language` argument. +# +# It does still look like it could be a literal block according to the literal +# rules, but we currently consider the `.. ` prefix to indicate that it is not +# a literal block. +def rst_literal_skipped_not_directive(): """ - Do cool stuff. + .. code-block:: - >>> x = '\"\"\"' + cool_stuff( 1 ) + + Done. """ pass -############################################################################### -# reStructuredText CODE EXAMPLES -# -# This section shows examples of docstrings that contain code snippets in -# reStructuredText formatted code blocks. +# In this test, we start a line with `.. `, which makes it look like it might +# be a directive. But instead continue it as if it was just some periods from +# the previous line, and then try to end it by starting a literal block. # -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks -# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30 -# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38 -############################################################################### - - -def rst_literal_simple(): +# But because of the `.. ` in the beginning, we wind up not treating this as a +# code snippet. The reST render I was using to test things does actually treat +# this as a code block, so we may be out of conformance here. +def rst_literal_skipped_possible_false_negative(): """ - Do cool stuff:: + This is a test. + .. This is a test:: - cool_stuff(1) + cool_stuff( 1 ) Done. """ pass -def rst_literal_simple_continued(): +# This tests that a doctest inside of a reST literal block doesn't get +# reformatted. It's plausible this isn't the right behavior, but it also seems +# like it might be the right behavior since it is a literal block. (The doctest +# makes the Python code invalid.) +def rst_literal_skipped_doctest(): """ Do cool stuff:: - def cool_stuff(x): - print(f"hi {x}") + >>> cool_stuff( 1 ) Done. """ pass -# Tests that we can end the literal block on the second -# to last line of the docstring. -def rst_literal_second_to_last(): +def rst_literal_skipped_markdown(): """ Do cool stuff:: - cool_stuff(1) + ```py + cool_stuff( 1 ) + ``` + + Done. """ pass -# Tests that we can end the literal block on the actual -# last line of the docstring. -def rst_literal_actually_last(): +def rst_directive_skipped_not_indented(): """ - Do cool stuff:: - - cool_stuff(1)""" - pass + .. code-block:: python + cool_stuff( 1 ) -def rst_literal_with_blank_lines(): + Done. """ - Do cool stuff:: + pass - def cool_stuff(x): - print(f"hi {x}") +def rst_directive_skipped_wrong_language(): + """ + .. code-block:: rust - def other_stuff(y): - print(y) + cool_stuff( 1 ) Done. """ pass -# Extra blanks should be preserved. -def rst_literal_extra_blanks(): +# This gets skipped for the same reason that the doctest in a literal block +# gets skipped. +def rst_directive_skipped_doctest(): """ - Do cool stuff:: + .. code-block:: python + + >>> cool_stuff( 1 ) + Done. + """ + pass - cool_stuff(1) +############################################################################### +# Markdown CODE EXAMPLES +# +# This section shows examples of docstrings that contain code snippets in +# Markdown fenced code blocks. +# +# See: https://spec.commonmark.org/0.30/#fenced-code-blocks +############################################################################### + +def markdown_simple(): + """ + Do cool stuff. + ```py + cool_stuff(1) + ``` Done. """ pass -# If a literal block is never properly ended (via a non-empty unindented line), -# then the end of the block should be the last non-empty line. And subsequent -# empty lines should be preserved as-is. -def rst_literal_extra_blanks_at_end(): +def markdown_simple_continued(): """ - Do cool stuff:: - + Do cool stuff. - cool_stuff(1) + ```python + def cool_stuff(x): + print(f"hi {x}") + ``` + Done. + """ + pass +# Tests that unlabeled Markdown fenced code blocks are assumed to be Python. +def markdown_unlabeled(): """ - pass + Do cool stuff. + ``` + cool_stuff(1) + ``` -# A literal block can contain many empty lines and it should not end the block -# if it continues. -def rst_literal_extra_blanks_in_snippet(): + Done. """ - Do cool stuff:: + pass - cool_stuff(1) +# Tests that fenced code blocks using tildes work. +def markdown_tildes(): + """ + Do cool stuff. - cool_stuff(2) + ~~~py + cool_stuff(1) + ~~~ Done. """ pass -# This tests that a unindented line appearing after an indented line (but where -# the indent is still beyond the minimum) gets formatted properly. -def rst_literal_subsequent_line_not_indented(): +# Tests that a longer closing fence is just fine and dandy. +def markdown_longer_closing_fence(): """ - Do cool stuff:: + Do cool stuff. - if True: - cool_stuff( - ''' - hiya''' - ) + ```py + cool_stuff(1) + `````` Done. """ pass -# This checks that if the first line in a code snippet has been indented with -# tabs, then so long as its "indentation length" is considered bigger than the -# line with `::`, it is reformatted as code. +# Tests that an invalid closing fence is treated as invalid. # -# (If your tabwidth is set to 4, then it looks like the code snippet -# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST -# itself also seems to recognize this as a code block, although it appears -# under-specified.) -def rst_literal_first_line_indent_uses_tabs_4spaces(): +# We embed it into a docstring so that the surrounding Python +# remains valid. +def markdown_longer_closing_fence(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff(1) + ```py + cool_stuff(1) + ''' + ```invalid + ''' + cool_stuff(2) + ``` Done. """ pass -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_4spaces_multiple(): +# Tests that one can nest fenced code blocks by using different numbers of +# backticks. +def markdown_nested_fences(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff(1) - cool_stuff(2) + `````` + do_something( + ''' + ``` + did i trick you? + ``` + ''' + ) + `````` Done. """ pass -# Another test with tabs, except in this case, if your tabwidth is less than -# 8, than the code snippet actually looks like its indent is *less* than the -# opening line with a `::`. One might presume this means that the code snippet -# is not treated as a literal block and thus not reformatted, but since we -# assume all tabs have tabwidth=8 when computing indentation length, the code -# snippet is actually seen as being more indented than the opening `::` line. -# As with the above example, reST seems to behave the same way here. -def rst_literal_first_line_indent_uses_tabs_8spaces(): +# Tests that an unclosed block gobbles up everything remaining in the +# docstring. When it's only empty lines, those are passed into the formatter +# and thus stripped. +def markdown_unclosed_empty_lines(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff(1) + ```py + cool_stuff(1)""" + pass - Done. + +# Tests that we can end the block on the second to last line of the +# docstring. +def markdown_second_to_last(): + """ + Do cool stuff. + + ```py + cool_stuff(1) + ``` """ pass -# Like the test above, but with multiple lines. -def rst_literal_first_line_indent_uses_tabs_8spaces_multiple(): +# Tests that an unclosed block with one extra line at the end is treated +# correctly. As per the CommonMark spec, an unclosed fenced code block contains +# everything following the opening fences. Since formatting the code snippet +# trims lines, the last empty line is removed here. +def markdown_second_to_last(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff(1) - cool_stuff(2) + ```py + cool_stuff(1)""" + pass - Done. + +# Tests that we can end the block on the actual last line of the docstring. +def markdown_actually_last(): """ + Do cool stuff. + + ```py + cool_stuff(1) + ```""" pass -# Tests that if two lines in a literal block are indented to the same level -# but by different means (tabs versus spaces), then we correctly recognize the -# block and format it. -def rst_literal_first_line_tab_second_line_spaces(): +# Tests that an unclosed block that ends on the last line of a docstring +# is handled correctly. +def markdown_unclosed_actually_last(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff(1) - cool_stuff(2) + ```py + cool_stuff(1)""" + pass + + +def markdown_with_blank_lines(): + """ + Do cool stuff. + + ```py + def cool_stuff(x): + print(f"hi {x}") + + + def other_stuff(y): + print(y) + ``` Done. """ pass -# Tests that when two lines in a code snippet have weird and inconsistent -# indentation, the code still gets formatted so long as the indent is greater -# than the indent of the `::` line. -# -# In this case, the minimum indent is 5 spaces (from the second line) where as -# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8). -# The minimum indent is stripped from each code line. Since tabs aren't -# divisible, the entire tab is stripped, which means the first and second lines -# wind up with the same level of indentation. -# -# An alternative behavior here would be that the tab is replaced with 3 spaces -# instead of being stripped entirely. The code snippet itself would then have -# inconsistent indentation to the point of being invalid Python, and thus code -# formatting would be skipped. -# -# I decided on the former behavior because it seems a bit easier to implement, -# but we might want to switch to the alternative if cases like this show up in -# the real world. ---AG -def rst_literal_odd_indentation(): +def markdown_first_line_indent_uses_tabs_4spaces(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff(1) - cool_stuff(2) + ```py + cool_stuff(1) + ``` Done. """ pass -# Tests that having a line with a lone `::` works as an introduction of a -# literal block. -def rst_literal_lone_colon(): +def markdown_first_line_indent_uses_tabs_4spaces_multiple(): """ Do cool stuff. - :: - - cool_stuff(1) + ```py + cool_stuff(1) + cool_stuff(2) + ``` Done. """ pass -def rst_directive_simple(): +def markdown_first_line_indent_uses_tabs_8spaces(): """ - .. code-block:: python + Do cool stuff. - cool_stuff(1) + ```py + cool_stuff(1) + ``` Done. """ pass -def rst_directive_case_insensitive(): +def markdown_first_line_indent_uses_tabs_8spaces_multiple(): """ - .. cOdE-bLoCk:: python + Do cool stuff. - cool_stuff(1) + ```py + cool_stuff(1) + cool_stuff(2) + ``` Done. """ pass -def rst_directive_sourcecode(): +def markdown_first_line_tab_second_line_spaces(): """ - .. sourcecode:: python + Do cool stuff. + ```py cool_stuff(1) + cool_stuff(2) + ``` Done. """ pass -def rst_directive_options(): +def markdown_odd_indentation(): """ - .. code-block:: python - :linenos: - :emphasize-lines: 2,3 - :name: blah blah + Do cool stuff. + ```py cool_stuff(1) cool_stuff(2) - cool_stuff(3) - cool_stuff(4) + ``` Done. """ pass -# In this case, since `pycon` isn't recognized as a Python code snippet, the -# docstring reformatter ignores it. But it then picks up the doctest and -# reformats it. -def rst_directive_doctest(): +# Extra blanks should be *not* be preserved (unlike reST) because they are part +# of the code snippet (per CommonMark spec), and thus get trimmed as part of +# code formatting. +def markdown_extra_blanks(): """ - .. code-block:: pycon + Do cool stuff. - >>> cool_stuff(1) + ```py + cool_stuff(1) + ``` Done. """ pass -# This checks that if the first non-empty line after the start of a literal -# block is not indented more than the line containing the `::`, then it is not -# treated as a code snippet. -def rst_literal_skipped_first_line_not_indented(): +# A block can contain many empty lines within it. +def markdown_extra_blanks_in_snippet(): """ - Do cool stuff:: + Do cool stuff. - cool_stuff( 1 ) + ```py + cool_stuff(1) + + + cool_stuff(2) + ``` Done. """ pass -# Like the test above, but inserts an indented line after the un-indented one. -# This should not cause the literal block to be resumed. -def rst_literal_skipped_first_line_not_indented_then_indented(): +def markdown_weird_closing(): """ - Do cool stuff:: + Code block with weirdly placed closing fences. - cool_stuff( 1 ) - cool_stuff( 2 ) + ```python + cool_stuff(1) + ``` + # The above fences look like it shouldn't close the block, but we + # allow it to. The fences below re-open a block (until the end of + # the docstring), but it's invalid Python and thus doesn't get + # reformatted. + a = 10 + ``` - Done. + Now the code block is closed """ pass -# This also checks that a code snippet is not reformatted when the indentation -# of the first line is not more than the line with `::`, but this uses tabs to -# make it a little more confounding. It relies on the fact that indentation -# length is computed by assuming a tabwidth equal to 8. reST also rejects this -# and doesn't treat it as a literal block. -def rst_literal_skipped_first_line_not_indented_tab(): +def markdown_over_indented(): """ - Do cool stuff:: - - cool_stuff( 1 ) - - Done. + A docstring + over intended + ```python + print(5) + ``` """ pass -# Like the previous test, but adds a second line. -def rst_literal_skipped_first_line_not_indented_tab_multiple(): +# Tests that an unclosed block gobbles up everything remaining in the +# docstring, even if it isn't valid Python. Since it isn't valid Python, +# reformatting fails and the entire thing is skipped. +def markdown_skipped_unclosed_non_python(): """ - Do cool stuff:: + Do cool stuff. + ```py cool_stuff( 1 ) - cool_stuff( 2 ) - Done. + I forgot to close the code block, and this is definitely not + Python. So nothing here gets formatted. """ pass -# Tests that a code block with a second line that is not properly indented gets -# skipped. A valid code block needs to have an empty line separating these. -# -# One trick here is that we need to make sure the Python code in the snippet is -# valid, otherwise it would be skipped because of invalid Python. -def rst_literal_skipped_subsequent_line_not_indented(): +# This has a Python snippet with a docstring that contains a closing fence. +# This splits the embedded docstring and makes the overall snippet invalid. +def markdown_skipped_accidental_closure(): """ - Do cool stuff:: + Do cool stuff. - if True: - cool_stuff( ''' - hiya''' ) + ```py + cool_stuff( 1 ) + ''' + ``` + ''' + ``` Done. """ pass -# In this test, we write what looks like a code-block, but it should be treated -# as invalid due to the missing `language` argument. +# When a line is unindented all the way out before the standard indent of the +# docstring, the code reformatting ends up interacting poorly with the standard +# docstring whitespace normalization logic. This is probably a bug, and we +# should probably treat the Markdown block as valid, but for now, we detect +# the unindented line and declare the block as invalid and thus do no code +# reformatting. # -# It does still look like it could be a literal block according to the literal -# rules, but we currently consider the `.. ` prefix to indicate that it is not -# a literal block. -def rst_literal_skipped_not_directive(): +# FIXME: Fixing this (if we think it's a bug) probably requires refactoring the +# docstring whitespace normalization to be aware of code snippets. Or perhaps +# plausibly, to do normalization *after* code snippets have been formatted. +def markdown_skipped_unindented_completely(): """ - .. code-block:: + Do cool stuff. - cool_stuff( 1 ) + ```py + cool_stuff( 1 ) + ``` - Done. + Done. """ pass -# In this test, we start a line with `.. `, which makes it look like it might -# be a directive. But instead continue it as if it was just some periods from -# the previous line, and then try to end it by starting a literal block. -# -# But because of the `.. ` in the beginning, we wind up not treating this as a -# code snippet. The reST render I was using to test things does actually treat -# this as a code block, so we may be out of conformance here. -def rst_literal_skipped_possible_false_negative(): +# This test is fallout from treating fenced code blocks with unindented lines +# as invalid. We probably should treat this as a valid block. Indeed, if we +# remove the logic that makes the `markdown_skipped_unindented_completely` test +# pass, then this code snippet will get reformatted correctly. +def markdown_skipped_unindented_somewhat(): """ - This is a test. - .. This is a test:: + Do cool stuff. - cool_stuff( 1 ) + ```py + cool_stuff( 1 ) + ``` Done. """ pass -# This tests that a doctest inside of a reST literal block doesn't get -# reformatted. It's plausible this isn't the right behavior, but it also seems -# like it might be the right behavior since it is a literal block. (The doctest -# makes the Python code invalid.) -def rst_literal_skipped_doctest(): +# This tests that if a Markdown block contains a line that has less of an +# indent than another line. +# +# There is some judgment involved in what the right behavior is here. We +# could "normalize" the indentation so that the minimum is the indent of the +# opening fence line. If we did that here, then the code snippet would become +# valid and format as Python. But at time of writing, we don't, which leads to +# inconsistent indentation and thus invalid Python. +def markdown_skipped_unindented_with_inconsistent_indentation(): """ - Do cool stuff:: + Do cool stuff. - >>> cool_stuff( 1 ) + ```py + cool_stuff( 1 ) + cool_stuff( 2 ) + ``` Done. """ pass -def rst_directive_skipped_not_indented(): +def markdown_skipped_doctest(): """ - .. code-block:: python + Do cool stuff. - cool_stuff( 1 ) + ```py + >>> cool_stuff( 1 ) + ``` Done. """ pass -def rst_directive_skipped_wrong_language(): +def markdown_skipped_rst_literal(): """ - .. code-block:: rust + Do cool stuff. + + ```py + And do this:: cool_stuff( 1 ) + ``` + Done. """ pass -# This gets skipped for the same reason that the doctest in a literal block -# gets skipped. -def rst_directive_skipped_doctest(): +def markdown_skipped_rst_directive(): """ + Do cool stuff. + + ```py .. code-block:: python - >>> cool_stuff( 1 ) + cool_stuff( 1 ) + + ``` Done. """