Improve docstring re-indentation handling
This addresses a few crashers, namely: * producing non-equivalent code due to mangling escaped newlines, * invalid hugging quote characters in the docstring body to the docstring outer triple quotes (causing a quadruple quote which is a syntax error), * lack of handling for docstrings that start on the same line as the `def`, and * invalid stripping of outer triple quotes when the docstring contained a string prefix. As a bonus, tests now also run when string normalization is disabled.
This commit is contained in:
parent
586d24236e
commit
9270a10f6f
@ -2037,13 +2037,20 @@ def visit_factor(self, node: Node) -> Iterator[Line]:
|
|||||||
yield from self.visit_default(node)
|
yield from self.visit_default(node)
|
||||||
|
|
||||||
def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
|
def visit_STRING(self, leaf: Leaf) -> Iterator[Line]:
|
||||||
# Check if it's a docstring
|
if is_docstring(leaf) and "\\\n" not in leaf.value:
|
||||||
if prev_siblings_are(
|
# We're ignoring docstrings with backslash newline escapes because changing
|
||||||
leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt]
|
# indentation of those changes the AST representation of the code.
|
||||||
) and is_multiline_string(leaf):
|
prefix = get_string_prefix(leaf.value)
|
||||||
prefix = " " * self.current_line.depth
|
lead_len = len(prefix) + 3
|
||||||
docstring = fix_docstring(leaf.value[3:-3], prefix)
|
tail_len = -3
|
||||||
leaf.value = leaf.value[0:3] + docstring + leaf.value[-3:]
|
indent = " " * 4 * self.current_line.depth
|
||||||
|
docstring = fix_docstring(leaf.value[lead_len:tail_len], indent)
|
||||||
|
if docstring:
|
||||||
|
if leaf.value[lead_len - 1] == docstring[0]:
|
||||||
|
docstring = " " + docstring
|
||||||
|
if leaf.value[tail_len + 1] == docstring[-1]:
|
||||||
|
docstring = docstring + " "
|
||||||
|
leaf.value = leaf.value[0:lead_len] + docstring + leaf.value[tail_len:]
|
||||||
normalize_string_quotes(leaf)
|
normalize_string_quotes(leaf)
|
||||||
|
|
||||||
yield from self.visit_default(leaf)
|
yield from self.visit_default(leaf)
|
||||||
@ -6608,6 +6615,26 @@ def patched_main() -> None:
|
|||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
def is_docstring(leaf: Leaf) -> bool:
|
||||||
|
if not is_multiline_string(leaf):
|
||||||
|
# For the purposes of docstring re-indentation, we don't need to do anything
|
||||||
|
# with single-line docstrings.
|
||||||
|
return False
|
||||||
|
|
||||||
|
if prev_siblings_are(
|
||||||
|
leaf.parent, [None, token.NEWLINE, token.INDENT, syms.simple_stmt]
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Multiline docstring on the same line as the `def`.
|
||||||
|
if prev_siblings_are(leaf.parent, [syms.parameters, token.COLON, syms.simple_stmt]):
|
||||||
|
# `syms.parameters` is only used in funcdefs and async_funcdefs in the Python
|
||||||
|
# grammar. We're safe to return True without further checks.
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def fix_docstring(docstring: str, prefix: str) -> str:
|
def fix_docstring(docstring: str, prefix: str) -> str:
|
||||||
# https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
|
# https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
|
||||||
if not docstring:
|
if not docstring:
|
||||||
@ -6631,7 +6658,6 @@ def fix_docstring(docstring: str, prefix: str) -> str:
|
|||||||
trimmed.append(prefix + stripped_line)
|
trimmed.append(prefix + stripped_line)
|
||||||
else:
|
else:
|
||||||
trimmed.append("")
|
trimmed.append("")
|
||||||
# Return a single string:
|
|
||||||
return "\n".join(trimmed)
|
return "\n".join(trimmed)
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,6 +81,35 @@ def single_line():
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def this():
|
||||||
|
r"""
|
||||||
|
'hey ho'
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def that():
|
||||||
|
""" "hey yah" """
|
||||||
|
|
||||||
|
|
||||||
|
def and_that():
|
||||||
|
"""
|
||||||
|
"hey yah" """
|
||||||
|
|
||||||
|
|
||||||
|
def and_this():
|
||||||
|
'''
|
||||||
|
"hey yah"'''
|
||||||
|
|
||||||
|
|
||||||
|
def believe_it_or_not_this_is_in_the_py_stdlib(): '''
|
||||||
|
"hey yah"'''
|
||||||
|
|
||||||
|
|
||||||
|
def ignored_docstring():
|
||||||
|
"""a => \
|
||||||
|
b"""
|
||||||
|
|
||||||
# output
|
# output
|
||||||
|
|
||||||
class MyClass:
|
class MyClass:
|
||||||
@ -164,3 +193,33 @@ def over_indent():
|
|||||||
def single_line():
|
def single_line():
|
||||||
"""But with a newline after it!"""
|
"""But with a newline after it!"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def this():
|
||||||
|
r"""
|
||||||
|
'hey ho'
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def that():
|
||||||
|
""" "hey yah" """
|
||||||
|
|
||||||
|
|
||||||
|
def and_that():
|
||||||
|
"""
|
||||||
|
"hey yah" """
|
||||||
|
|
||||||
|
|
||||||
|
def and_this():
|
||||||
|
'''
|
||||||
|
"hey yah"'''
|
||||||
|
|
||||||
|
|
||||||
|
def believe_it_or_not_this_is_in_the_py_stdlib():
|
||||||
|
'''
|
||||||
|
"hey yah"'''
|
||||||
|
|
||||||
|
|
||||||
|
def ignored_docstring():
|
||||||
|
"""a => \
|
||||||
|
b"""
|
@ -496,6 +496,11 @@ def test_docstring(self) -> None:
|
|||||||
self.assertFormatEqual(expected, actual)
|
self.assertFormatEqual(expected, actual)
|
||||||
black.assert_equivalent(source, actual)
|
black.assert_equivalent(source, actual)
|
||||||
black.assert_stable(source, actual, DEFAULT_MODE)
|
black.assert_stable(source, actual, DEFAULT_MODE)
|
||||||
|
mode = replace(DEFAULT_MODE, string_normalization=False)
|
||||||
|
not_normalized = fs(source, mode=mode)
|
||||||
|
self.assertFormatEqual(expected, not_normalized)
|
||||||
|
black.assert_equivalent(source, not_normalized)
|
||||||
|
black.assert_stable(source, not_normalized, mode=mode)
|
||||||
|
|
||||||
def test_long_strings(self) -> None:
|
def test_long_strings(self) -> None:
|
||||||
"""Tests for splitting long strings."""
|
"""Tests for splitting long strings."""
|
||||||
|
Loading…
Reference in New Issue
Block a user