diff --git a/CHANGES.md b/CHANGES.md index a75b54d8d81..86e820a6fd9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ ### Preview style - Fix merging implicit multiline strings that have inline comments (#3956) +- Allow empty first line after block open before a comment or compound statement (#3967) ### Configuration diff --git a/src/black/lines.py b/src/black/lines.py index 6acc95e7a7e..a73c429e3d9 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -24,6 +24,8 @@ STANDALONE_COMMENT, TEST_DESCENDANTS, child_towards, + is_docstring, + is_funcdef, is_import, is_multiline_string, is_one_sequence_between, @@ -686,7 +688,30 @@ def _maybe_empty_lines(self, current_line: Line) -> Tuple[int, int]: return 0, 1 return before, 1 - if self.previous_line and self.previous_line.opens_block: + is_empty_first_line_ok = ( + Preview.allow_empty_first_line_before_new_block_or_comment + in current_line.mode + and ( + # If it's a standalone comment + current_line.leaves[0].type == STANDALONE_COMMENT + # If it opens a new block + or current_line.opens_block + # If it's a triple quote comment (but not at the start of a funcdef) + or ( + is_docstring(current_line.leaves[0]) + and self.previous_line + and self.previous_line.leaves[0] + and self.previous_line.leaves[0].parent + and not is_funcdef(self.previous_line.leaves[0].parent) + ) + ) + ) + + if ( + self.previous_line + and self.previous_line.opens_block + and not is_empty_first_line_ok + ): return 0, 0 return before, 0 diff --git a/src/black/mode.py b/src/black/mode.py index 309f22dae94..4effeef3e7c 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -190,6 +190,7 @@ class Preview(Enum): module_docstring_newlines = auto() accept_raw_docstrings = auto() fix_power_op_line_length = auto() + allow_empty_first_line_before_new_block_or_comment = auto() class Deprecated(UserWarning): diff --git a/src/black/nodes.py b/src/black/nodes.py index edd201a21e9..b2e96cb9edf 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -718,6 +718,10 @@ def is_multiline_string(leaf: Leaf) -> bool: return has_triple_quotes(leaf.value) and "\n" in leaf.value +def is_funcdef(node: Node) -> bool: + return node.type == syms.funcdef + + def is_stub_suite(node: Node) -> bool: """Return True if `node` is a suite with a stub body.""" diff --git a/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py b/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py new file mode 100644 index 00000000000..96c1433c110 --- /dev/null +++ b/tests/data/cases/preview_allow_empty_first_line_in_special_cases.py @@ -0,0 +1,106 @@ +# flags: --preview +def foo(): + """ + Docstring + """ + + # Here we go + if x: + + # This is also now fine + a = 123 + + else: + # But not necessary + a = 123 + + if y: + + while True: + + """ + Long comment here + """ + a = 123 + + if z: + + for _ in range(100): + a = 123 + else: + + try: + + # this should be ok + a = 123 + except: + + """also this""" + a = 123 + + +def bar(): + + if x: + a = 123 + + +def baz(): + + # OK + if x: + a = 123 + +# output + +def foo(): + """ + Docstring + """ + + # Here we go + if x: + + # This is also now fine + a = 123 + + else: + # But not necessary + a = 123 + + if y: + + while True: + + """ + Long comment here + """ + a = 123 + + if z: + + for _ in range(100): + a = 123 + else: + + try: + + # this should be ok + a = 123 + except: + + """also this""" + a = 123 + + +def bar(): + + if x: + a = 123 + + +def baz(): + + # OK + if x: + a = 123