diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index 5bc38d4960f15b..1c8825b3aff359 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -28,6 +28,7 @@ countme = "3.0.1" itertools = { workspace = true } memchr = { workspace = true } once_cell = { workspace = true } +regex = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, optional = true } schemars = { workspace = true, optional = true } 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 7d827d2cda0538..112fcf6e39b8ff 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 @@ -344,3 +344,411 @@ def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): >>> 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 + + +# 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 + + +# 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 + + +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 diff --git a/crates/ruff_python_formatter/src/expression/string/docstring.rs b/crates/ruff_python_formatter/src/expression/string/docstring.rs index 2fd73c75485340..000448d31397c8 100644 --- a/crates/ruff_python_formatter/src/expression/string/docstring.rs +++ b/crates/ruff_python_formatter/src/expression/string/docstring.rs @@ -4,6 +4,8 @@ use std::{borrow::Cow, collections::VecDeque}; +use {once_cell::sync::Lazy, regex::Regex}; + use { ruff_formatter::{write, IndentStyle, Printed}, ruff_python_trivia::PythonWhitespace, @@ -332,6 +334,19 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> { )?; } } + CodeExampleKind::Rst(litblock) => { + let Some(min_indent) = litblock.min_indent else { + continue; + }; + // This looks suspicious, but it's consistent with the whitespace + // normalization that will occur anyway. + let indent = " ".repeat(min_indent.to_usize()); + for docline in formatted_lines { + self.print_one( + &docline.map(|line| std::format!("{indent}{line}")), + )?; + } + } } } } @@ -615,6 +630,13 @@ impl<'src> CodeExample<'src> { }; self.kind = Some(CodeExampleKind::Doctest(doctest)); } + Some(CodeExampleKind::Rst(litblock)) => { + let Some(litblock) = litblock.add_code_line(original, queue) else { + self.add_start(original, queue); + return; + }; + self.kind = Some(CodeExampleKind::Rst(litblock)); + } } } @@ -645,6 +667,9 @@ impl<'src> CodeExample<'src> { if let Some(doctest) = CodeExampleDoctest::new(original) { self.kind = Some(CodeExampleKind::Doctest(doctest)); queue.push_back(CodeExampleAddAction::Kept); + } else if let Some(litblock) = CodeExampleRst::new(original) { + self.kind = Some(CodeExampleKind::Rst(litblock)); + queue.push_back(CodeExampleAddAction::Print { original }); } else { queue.push_back(CodeExampleAddAction::Print { original }); } @@ -665,6 +690,12 @@ enum CodeExampleKind<'src> { /// /// [regex matching]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L611-L622 Doctest(CodeExampleDoctest<'src>), + /// Code found from a reStructuredText "[literal block]" or "[code block + /// directive]". + /// + /// [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>), } impl<'src> CodeExampleKind<'src> { @@ -675,6 +706,7 @@ impl<'src> CodeExampleKind<'src> { fn code(&mut self) -> &[CodeExampleLine<'src>] { match *self { CodeExampleKind::Doctest(ref doctest) => &doctest.lines, + CodeExampleKind::Rst(ref mut litblock) => litblock.code(), } } @@ -687,6 +719,7 @@ impl<'src> CodeExampleKind<'src> { fn into_code(self) -> Vec> { match self { CodeExampleKind::Doctest(doctest) => doctest.lines, + CodeExampleKind::Rst(litblock) => litblock.lines, } } } @@ -779,6 +812,311 @@ impl<'src> CodeExampleDoctest<'src> { } } +/// State corresponding to a single reStructuredText literal block or +/// code-block directive. +/// +/// While a literal block and code-block directive are technically two +/// different reStructuredText constructs, we use one type to represent +/// both because they are exceptionally similar. Basically, they are +/// the same with two main differences: +/// +/// 1. Literal blocks are began with a line that ends with `::`. Code block +/// directives are began with a line like `.. code-block:: python`. +/// 2. Code block directives permit a list of options as a "field list" +/// immediately after the opening line. Literal blocks have no options. +/// +/// Otherwise, everything else, including the indentation structure, is the +/// same. +#[derive(Debug)] +struct CodeExampleRst<'src> { + /// The lines that have been seen so far that make up the block. + lines: Vec>, + /// The indent of the line "opening" this block. It can either be the + /// indent of a line ending with `::` (for a literal block) or the indent + /// of a line starting with `.. ` (a directive). + /// + /// The content body of a block needs to be indented more than the line + /// opening the block, so we use this indentation to look for indentation + /// that is "more than" it. + opening_indent: TextSize, + /// The minimum indent of the block. + /// + /// This is `None` until the first such line is seen. If no such line is + /// found, then we consider it an invalid block and bail out of trying to + /// find a code snippet. Otherwise, we update this indentation as we see + /// lines in the block with less indentation. (Usually, the minimum is the + /// indentation of the first block, but this is not required.) + /// + /// By construction, all lines part of the block must have at least this + /// indentation. Additionally, it is guaranteed that the indentation length + /// of the opening indent is strictly less than the indentation of the + /// minimum indent. Namely, the block ends once we find a line that has + /// been unindented to at most the indent of the opening line. + /// + /// When the code snippet has been extracted, it is re-built before being + /// reformatted. The minimum indent is stripped from each line when it is + /// re-built. + min_indent: Option, + /// Whether this is a directive block or not. When not a directive, this is + /// a literal block. The main difference between them is that they start + /// differently. A literal block is started merely by trailing a line with + /// `::`. A directive block is started with `.. code-block:: python`. + /// + /// The other difference is that directive blocks can have options + /// (represented as a reStructuredText "field list") after the beginning of + /// the directive and before the body content of the directive. + is_directive: bool, +} + +impl<'src> CodeExampleRst<'src> { + /// Looks for the start of a reStructuredText [literal block] or [code + /// block directive]. + /// + /// If the start of a block is found, then this returns a correctly + /// initialized reStructuredText block. Callers should print the line as + /// given as it is not retained as part of the block. + /// + /// [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 + fn new(original: InputDocstringLine<'src>) -> Option { + let (opening_indent, rest) = indent_with_suffix(original.line); + if rest.starts_with(".. ") { + if let Some(litblock) = CodeExampleRst::new_code_block(original) { + return Some(litblock); + } + // In theory, we could still have something that looks like a literal block, + // but if the line starts with `.. `, then it seems like it probably shouldn't + // be a literal block. For example: + // + // .. code-block:: + // + // cool_stuff( 1 ) + // + // The above is not valid because the `language` argument is missing from + // the `code-block` directive. Because of how we handle it here, the above + // is not treated as a code snippet. + // + // ... but it + return None; + } + // At this point, we know we didn't find a code block, so the only + // thing we can hope for is a literal block which must end with a `::`. + if !rest.trim_end().ends_with("::") { + return None; + } + Some(CodeExampleRst { + lines: vec![], + opening_indent: indentation_length(opening_indent), + min_indent: None, + is_directive: false, + }) + } + + /// Attempts to create a new reStructuredText code example from a + /// `code-block` or `sourcecode` directive. If one couldn't be found, then + /// `None` is returned. + fn new_code_block(original: InputDocstringLine<'src>) -> Option { + // This regex attempts to parse the start of a reStructuredText code + // block [directive]. From the reStructuredText spec: + // + // > Directives are indicated by an explicit markup start (".. ") + // > followed by the directive type, two colons, and whitespace + // > (together called the "directive marker"). Directive types + // > are case-insensitive single words (alphanumerics plus + // > isolated internal hyphens, underscores, plus signs, colons, + // > and periods; no whitespace). + // + // The language names matched here (e.g., `python` or `py`) are taken + // from the [Pygments lexer names], which is referenced from the docs + // for the [code-block] directive. + // + // [directives]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#directives + // [Pygments lexer names]: https://pygments.org/docs/lexers/ + // [code-block]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block + static DIRECTIVE_START: Lazy = Lazy::new(|| { + Regex::new( + r"(?m)^\s*\.\. \s*(?i:code-block|sourcecode)::\s*(?i:python|py|python3|py3)$", + ) + .unwrap() + }); + if !DIRECTIVE_START.is_match(original.line) { + return None; + } + Some(CodeExampleRst { + lines: vec![], + opening_indent: indentation_length(original.line), + min_indent: None, + is_directive: true, + }) + } + + /// Returns the code collected in this example as a sequence of lines. + /// + /// The lines returned have the minimum indentation stripped from their + /// prefix. Based on the definition of minimum indentation, this implies + /// there is at least one line in the slice returned with no whitespace + /// prefix. + fn code(&mut self) -> &[CodeExampleLine<'src>] { + let Some(min_indent) = self.min_indent else { + return &[]; + }; + for line in &mut self.lines { + line.code = if line.original.line.trim().is_empty() { + "" + } else { + indentation_trim(min_indent, line.original.line) + }; + } + &self.lines + } + + /// Attempts to add the given line from a docstring to the reStructuredText + /// code snippet being collected. + /// + /// This takes ownership of `self`, and if ownership is returned to the + /// caller, that means the caller should continue trying to add lines to + /// this code snippet. Otherwise, if ownership is not returned, then this + /// implies at least one action was added to the give queue to either reset + /// the code block or format. That is, the code snippet was either found to + /// be invalid or it was completed and should be reformatted. + /// + /// Note that actions may be added even if ownership is returned. For + /// example, empty lines immediately preceding the actual code snippet will + /// be returned back as an action to print them verbatim, but the caller + /// should still continue to try to add lines to this code snippet. + fn add_code_line( + mut self, + original: InputDocstringLine<'src>, + queue: &mut VecDeque>, + ) -> Option> { + // If we haven't started populating the minimum indent yet, then + // we haven't found the first code line and may need to find and + // pass through leading empty lines. + let Some(min_indent) = self.min_indent else { + return self.add_first_line(original, queue); + }; + let (indent, rest) = indent_with_suffix(original.line); + if rest.is_empty() { + // This is the standard way we close a block: when we see + // an empty line followed by an unindented line. + if let Some(next) = original.next { + let (next_indent, _) = indent_with_suffix(next); + if indentation_length(next_indent) <= self.opening_indent { + queue.push_back(self.into_format_action()); + return None; + } + } else { + queue.push_back(self.into_format_action()); + return None; + } + self.push(original); + queue.push_back(CodeExampleAddAction::Kept); + return Some(self); + } + let indent_len = indentation_length(indent); + if indent_len <= self.opening_indent { + // If we find an unindented non-empty line at the same (or less) + // indentation of the opening line at this point, then we know it + // must be wrong because we didn't see it immediately following an + // empty line. + queue.push_back(self.into_reset_action()); + return None; + } else if indent_len < min_indent { + // While the minimum indent is usually the indentation of the first + // line in a code snippet, it is not guaranteed to be the case. + // And indeed, reST is happy to let blocks have a first line whose + // indentation is greater than a subsequent line in the block. The + // only real restriction is that every line in the block must be + // indented at least past the indentation of the `::` line. + self.min_indent = Some(indent_len); + } + self.push(original); + queue.push_back(CodeExampleAddAction::Kept); + Some(self) + } + + /// Looks for the first line in a literal or code block. + /// + /// If a first line is found, then this returns true. Otherwise, an empty + /// line has been found and the caller should pass it through to the + /// docstring unchanged. (Empty lines are allowed to precede a + /// block. And there must be at least one of them.) + /// + /// If the given line is invalid for a reStructuredText block (i.e., no + /// empty lines seen between the opening line), then an error variant is + /// returned. In this case, callers should bail out of parsing this code + /// example. + /// + /// When this returns `true`, it is guaranteed that `self.min_indent` is + /// set to a non-None value. + /// + /// # Panics + /// + /// Callers must only call this when the first indentation has not yet been + /// found. If it has, then this panics. + fn add_first_line( + mut self, + original: InputDocstringLine<'src>, + queue: &mut VecDeque>, + ) -> Option> { + assert!(self.min_indent.is_none()); + + // While the rst spec isn't completely clear on this point, through + // experimentation, I found that multiple empty lines before the first + // non-empty line are ignored. + let (indent, rest) = indent_with_suffix(original.line); + if rest.is_empty() { + queue.push_back(CodeExampleAddAction::Print { original }); + return Some(self); + } + // Ignore parameters in field lists. These can only occur in + // directives, not literal blocks. + if self.is_directive && is_rst_option(rest) { + queue.push_back(CodeExampleAddAction::Print { original }); + return Some(self); + } + let min_indent = indentation_length(indent); + // At this point, we found a non-empty line. The only thing we require + // is that its indentation is strictly greater than the indentation of + // the line containing the `::`. Otherwise, we treat this as an invalid + // block and bail. + if min_indent <= self.opening_indent { + queue.push_back(self.into_reset_action()); + return None; + } + self.min_indent = Some(min_indent); + self.push(original); + queue.push_back(CodeExampleAddAction::Kept); + Some(self) + } + + /// Pushes the given line as part of this code example. + fn push(&mut self, original: InputDocstringLine<'src>) { + // N.B. We record the code portion as identical to the original line. + // When we go to reformat the code lines, we change them by removing + // the `min_indent`. This design is necessary because the true value of + // `min_indent` isn't known until the entire block has been parsed. + let code = original.line; + self.lines.push(CodeExampleLine { original, code }); + } + + /// Consume this block and turn it into a formatting action. + fn into_format_action(self) -> CodeExampleAddAction<'src> { + CodeExampleAddAction::Format { + kind: CodeExampleKind::Rst(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 block, but later determined that it wasn't a valid + /// block. + fn into_reset_action(self) -> CodeExampleAddAction<'src> { + CodeExampleAddAction::Reset { code: self.lines } + } +} + /// A single line in a code example found in a docstring. /// /// A code example line exists prior to formatting, and is thus in full @@ -903,6 +1241,58 @@ fn indentation_length(line: &str) -> TextSize { TextSize::new(indentation) } +/// Trims at most `indent_len` indentation from the beginning of `line`. +/// +/// This treats indentation in precisely the same way as `indentation_length`. +/// As such, it is expected that `indent_len` is computed from +/// `indentation_length`. This is useful when one needs to trim some minimum +/// level of indentation from a code snippet collected from a docstring before +/// attempting to reformat it. +fn indentation_trim(indent_len: TextSize, mut line: &str) -> &str { + let mut seen_indent_len = 0u32; + for char in line.chars() { + if seen_indent_len >= indent_len.to_u32() { + return line; + } + if char == '\t' { + // Pad to the next multiple of tab_width + seen_indent_len += 8 - (seen_indent_len.rem_euclid(8)); + line = &line[1..]; + } else if char.is_whitespace() { + seen_indent_len += u32::from(char.text_len()); + line = &line[char.len_utf8()..]; + } else { + break; + } + } + line +} + +/// Returns the indentation of the given line and everything following it. +fn indent_with_suffix(line: &str) -> (&str, &str) { + let suffix = line.trim_start(); + let indent_len = line + .len() + .checked_sub(suffix.len()) + .expect("suffix <= line"); + let indent = &line[..indent_len]; + (indent, suffix) +} + +/// Returns true if this line looks like a reStructuredText option in a +/// field list. +/// +/// That is, a line that looks like `:name: optional-value`. +fn is_rst_option(line: &str) -> bool { + let line = line.trim_start(); + if !line.starts_with(':') { + return false; + } + line.chars() + .take_while(|ch| !ch.is_whitespace()) + .any(|ch| ch == ':') +} + #[cfg(test)] mod tests { use ruff_text_size::TextSize; diff --git a/crates/ruff_python_formatter/tests/normalizer.rs b/crates/ruff_python_formatter/tests/normalizer.rs index d0f2ba0b9c2342..8f01694468dcbd 100644 --- a/crates/ruff_python_formatter/tests/normalizer.rs +++ b/crates/ruff_python_formatter/tests/normalizer.rs @@ -60,7 +60,7 @@ impl Transformer for Normalizer { } fn visit_string_literal(&self, string_literal: &mut ast::StringLiteral) { - static STRIP_CODE_SNIPPETS: Lazy = Lazy::new(|| { + static STRIP_DOC_TESTS: Lazy = Lazy::new(|| { Regex::new( r#"(?mx) ( @@ -75,14 +75,27 @@ impl Transformer for Normalizer { ) .unwrap() }); + static STRIP_RST_BLOCKS: Lazy = Lazy::new(|| { + // This is kind of unfortunate, but it's pretty tricky (likely + // impossible) to detect a reStructuredText block with a simple + // regex. So we just look for the start of a block and remove + // everything after it. Talk about a hammer. + Regex::new(r#"::(?s:.*)"#).unwrap() + }); // Start by (1) stripping everything that looks like a code // snippet, since code snippets may be completely reformatted if // they are Python code. - string_literal.value = STRIP_CODE_SNIPPETS + string_literal.value = STRIP_DOC_TESTS + .replace_all( + &string_literal.value, + "\n", + ) + .into_owned(); + string_literal.value = STRIP_RST_BLOCKS .replace_all( &string_literal.value, - "\n", + "\n", ) .into_owned(); // Normalize a string by (2) stripping any leading and trailing space from each 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 82e2d7cff4efb5..d3c47fd9ed757f 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 @@ -350,6 +350,414 @@ def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): >>> 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 + + +# 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 + + +# 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 + + +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 ``` ## Outputs @@ -715,80 +1123,488 @@ def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): >>> 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:: + cool_stuff( 1 ) -# 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 +def rst_literal_simple_continued(): + """ + Do cool stuff:: -# Test that we support multiple directly adjacent -# doctests. -def doctest_adjacent(): - """ - Do cool stuff. + def cool_stuff( x ): + print( f"hi {x}" ); - >>> cool_stuff( x ) - >>> cool_stuff( y ) - 2 - """ - pass + Done. + """ + 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 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( x ) - """ - pass + cool_stuff( 1 ) + """ + pass -# Test that a doctest that continues to the last non-whitespace line of +# 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 + + +# 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 + + +# 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 + + +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 +``` + + +### 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(): """ @@ -1064,30 +1880,1211 @@ def doctest_skipped_fstring(): f""" Do cool stuff. - >>> cool_stuff( 1 ) - 2 - """ - pass + >>> 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 + + +# 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 + + +# 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 + + +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 +``` + + +### 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 + + +# 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 + + +# 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 + + +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python + + cool_stuff( 1 ) + + Done. + """ + pass + +def rst_directive_skipped_wrong_language(): + """ + .. code-block:: rust -# 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 ``` -### Output 3 +### Output 4 ``` indent-style = tab line-width = 88 -indent-width = 8 +indent-width = 4 quote-style = Double line-ending = LineFeed magic-trailing-comma = Respect @@ -1445,369 +3442,412 @@ def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): >>> x = '\"\"\"' """ pass -``` - -### Output 4 -``` -indent-style = tab -line-width = 88 -indent-width = 4 -quote-style = Double -line-ending = LineFeed -magic-trailing-comma = Respect -docstring-code = Disabled -preview = Disabled -``` -```python ############################################################################### -# 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(): + +def rst_literal_simple(): """ - Do cool stuff. + Do cool stuff:: - >>> cool_stuff( 1 ) - 2 + cool_stuff( 1 ) + + Done. """ pass -# Another simple test, but one where the Python code -# extends over multiple lines. -def doctest_simple_continued(): +def rst_literal_simple_continued(): """ - Do cool stuff. + Do cool stuff:: - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 + def cool_stuff( x ): + print( f"hi {x}" ); + + Done. """ pass -# Test that we support multiple directly adjacent -# doctests. -def doctest_adjacent(): +# 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( x ) - >>> cool_stuff( y ) - 2 + cool_stuff( 1 ) """ pass -# Test that a doctest on the last non-whitespace line of a docstring -# reformats correctly. -def doctest_last_line(): +# 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( x ) + 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 -# Test that a doctest that continues to the last non-whitespace line of -# a docstring reformats correctly. -def doctest_last_line_continued(): +# Extra blanks should be preserved. +def rst_literal_extra_blanks(): """ - Do cool stuff. + Do cool stuff:: - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); + + cool_stuff( 1 ) + + + Done. """ pass -# Test that a doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line(): +# 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:: - >>> cool_stuff( x )""" + if True: + cool_stuff( ''' + hiya''' ) + + Done. + """ pass -# Test that a continued doctest on the real last line of a docstring reformats -# correctly. -def doctest_really_last_line_continued(): +# 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:: - >>> cool_stuff( x ) - ... more( y )""" + cool_stuff( 1 ) + + Done. + """ pass -# Test that a doctest is correctly identified and formatted with a blank -# continuation line. -def doctest_blank_continued(): +# 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 ) + + 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(): +# 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:: - >>> 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_8spaces_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(): +# 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 = '''tricksy''' + cool_stuff( 1 ) + cool_stuff( 2 ) + + 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(): +# 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. + 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 having a line with a lone `::` works as an introduction of a +# literal block. +def rst_literal_lone_colon(): """ Do cool stuff. - >>> x = '''\"\"\"''' + :: + + cool_stuff( 1 ) + + Done. """ pass -# Tests that inverting the triple quoting works as expected. -def doctest_with_triple_inverted(): - ''' - Do cool stuff. +def rst_directive_simple(): + """ + .. code-block:: python - >>> x = """tricksy""" - ''' + cool_stuff( 1 ) + + Done. + """ pass -# Tests that inverting the triple quoting with an f-string works as -# expected. -def doctest_with_triple_inverted_fstring(): - ''' - Do cool stuff. +def rst_directive_case_insensitive(): + """ + .. cOdE-bLoCk:: python - >>> 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_sourcecode(): + """ + .. sourcecode:: 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_options(): """ - Do cool stuff. + .. code-block:: python + :linenos: + :emphasize-lines: 2,3 + :name: blah blah - >>> assert ("Easy!") - >>> import math - >>> math.floor( 1.9 ) - 1 + cool_stuff( 1 ) + cool_stuff( 2 ) + cool_stuff( 3 ) + cool_stuff( 4 ) + + 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(): +# 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:: - 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(): +# 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( x ): - 2 + cool_stuff( 1 ) + cool_stuff( 2 ) + + Done. """ pass -# Checks that a simple doctest that is continued over multiple lines, -# but is invalid, gets skipped. -def doctest_skipped_simple_continued(): +# 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:: - >>> 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(): +# 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:: - >>> def cool_stuff( x ): - ... print( f"hi {x}" ); - hi 2 + cool_stuff( 1 ) + cool_stuff( 2 ) + + 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(): +# 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. + Do cool stuff:: - >>> def cool_stuff( x ): - ... print( x ) - ... print( f"hi {x}" ); - hi 2 + if True: + cool_stuff( ''' + hiya''' ) + + 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(): +# 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:: - >>> 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 )" +# 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 -# 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. +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python - >>> cool_stuff( 1 ) - 2 - """ + 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(): +def rst_directive_skipped_wrong_language(): """ - Do cool stuff. + .. code-block:: rust - >>> x = '\"\"\"' + cool_stuff( 1 ) + + Done. """ pass ``` @@ -2124,55 +4164,466 @@ def doctest_skipped_partial_inconsistent_indent(): """ Do cool stuff. - >>> def cool_stuff(x): - ... print(x) - ... print( f"hi {x}" ); - hi 2 + >>> 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 + + +# 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 + + +# 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 -# 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(): +# 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:: - >>> 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 )" +# 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 -# 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. +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python - >>> cool_stuff( 1 ) - 2 + 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(): +def rst_directive_skipped_wrong_language(): """ - Do cool stuff. + .. code-block:: rust - >>> x = '\"\"\"' + cool_stuff( 1 ) + + Done. """ pass ``` @@ -2510,34 +4961,445 @@ def doctest_skipped_triple_incorrect(): pass -# Tests that a doctest on a single line is skipped. -def doctest_skipped_one_line(): - ">>> foo( x )" +# 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 + + +# 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 + + +# 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 -# 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. +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python - >>> cool_stuff( 1 ) - 2 - """ + 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(): +def rst_directive_skipped_wrong_language(): """ - Do cool stuff. + .. code-block:: rust - >>> x = '\"\"\"' + cool_stuff( 1 ) + + Done. """ pass ``` @@ -2846,63 +5708,474 @@ def doctest_skipped_inconsistent_indent(): 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(): +# 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 + + +# 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 + + +# 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. + Do cool stuff:: - >>> def cool_stuff(x): - ... print(x) - ... print( f"hi {x}" ); - hi 2 + if True: + cool_stuff( ''' + hiya''' ) + + 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(): +# 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:: - >>> 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 )" +# 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 -# 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. +def rst_directive_skipped_not_indented(): + """ + .. code-block:: python - >>> cool_stuff( 1 ) - 2 - """ + 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(): +def rst_directive_skipped_wrong_language(): """ - Do cool stuff. + .. code-block:: rust - >>> x = '\"\"\"' + cool_stuff( 1 ) + + Done. """ pass ``` @@ -3270,6 +6543,417 @@ def doctest_invalid_skipped_with_triple_double_in_single_quote_string(): >>> 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 + + +# 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 + + +# 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 + + +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 ```