From 66a6db9bd33d8a09eeaad816db288c5e8c63ef6a Mon Sep 17 00:00:00 2001 From: Espen Nilsen Date: Tue, 9 Sep 2025 19:02:03 +0200 Subject: [PATCH] Support multiple_blank_lines_between_functions --- README.md | 15 +++++++++++ yapf/pytree/blank_line_calculator.py | 38 +++++++++++++++++++++++++++- yapf/pytree/pytree_utils.py | 31 +++++++++++++++++++++++ yapf/yapflib/style.py | 14 ++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bb6b2277..a07607d71 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,21 @@ optional arguments: pass ``` +#### `BLANK_LINES_BETWEEN_CLASS_DEFS` + +> Sets the number of desired blank lines between methods inside +> class definitions. For example: + +```python + class Foo: + def method1(): + pass + # <------ multiple + # <------ blank lines here + def method2(): + pass +``` + #### `BLANK_LINE_BEFORE_CLASS_DOCSTRING` > Insert a blank line before a class-level docstring. diff --git a/yapf/pytree/blank_line_calculator.py b/yapf/pytree/blank_line_calculator.py index 32faaa2f4..19eb71aad 100644 --- a/yapf/pytree/blank_line_calculator.py +++ b/yapf/pytree/blank_line_calculator.py @@ -62,16 +62,25 @@ def __init__(self): self.last_comment_lineno = 0 self.last_was_decorator = False self.last_was_class_or_function = False + self._prev_stmt = None def Visit_simple_stmt(self, node): # pylint: disable=invalid-name self.DefaultNodeVisit(node) if node.children[0].type == grammar_token.COMMENT: self.last_comment_lineno = node.children[0].lineno + else: + # Do NOT set _prev_stmt on pure comment lines; keep the last real stmt. + self._prev_stmt = node def Visit_decorator(self, node): # pylint: disable=invalid-name + func = _DecoratedFuncdef(node) if (self.last_comment_lineno and self.last_comment_lineno == node.children[0].lineno - 1): _SetNumNewlines(node.children[0], _NO_BLANK_LINES) + elif self.last_was_decorator: + _SetNumNewlines(node.children[0], _NO_BLANK_LINES) + elif func is not None and self._prev_stmt is not None and _MethodsInSameClass(self._prev_stmt, func): + _SetNumNewlines(node.children[0], max(_ONE_BLANK_LINE, 1 + style.Get('BLANK_LINES_BETWEEN_CLASS_DEFS'))) else: _SetNumNewlines(node.children[0], self._GetNumNewlines(node)) for child in node.children: @@ -87,6 +96,7 @@ def Visit_classdef(self, node): # pylint: disable=invalid-name self.Visit(child) self.class_level -= 1 self.last_was_class_or_function = True + self._prev_stmt = node def Visit_funcdef(self, node): # pylint: disable=invalid-name self.last_was_class_or_function = False @@ -103,6 +113,7 @@ def Visit_funcdef(self, node): # pylint: disable=invalid-name self.Visit(child) self.function_level -= 1 self.last_was_class_or_function = True + self._prev_stmt = node def DefaultNodeVisit(self, node): """Override the default visitor for Node. @@ -156,7 +167,11 @@ def _GetNumNewlines(self, node): return _NO_BLANK_LINES elif self._IsTopLevel(node): return 1 + style.Get('BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION') - return _ONE_BLANK_LINE + elif self._prev_stmt is not None and _MethodsInSameClass(self._prev_stmt, node): + # Only between consecutive methods *in the same class*. + # Keep at least one blank line as a floor (to avoid 0 if user misconfigures). + return max(_ONE_BLANK_LINE, 1 + style.Get('BLANK_LINES_BETWEEN_CLASS_DEFS')) + return _NO_BLANK_LINES def _IsTopLevel(self, node): return (not (self.class_level or self.function_level) and @@ -175,3 +190,24 @@ def _StartsInZerothColumn(node): def _AsyncFunction(node): return (node.prev_sibling and node.prev_sibling.type == grammar_token.ASYNC) + + +def _MethodsInSameClass(prev_node, curr_node): + # 1) Walk up from each node to find the nearest *enclosing function* (def …). + prev_func = pytree_utils.EnclosingFunc(prev_node) + curr_func = pytree_utils.EnclosingFunc(curr_node) + + # 2) If either enclosing thing is not actually a function definition, bail out. + if not (pytree_utils.IsFuncDef(prev_func) and pytree_utils.IsFuncDef(curr_func)): + return False + + # 3) From each function, walk up to find the *enclosing class* (class …). + prev_cls = pytree_utils.EnclosingClass(prev_func.parent) + curr_cls = pytree_utils.EnclosingClass(curr_func.parent) + + # 4) True only if both functions live inside a class, and it’s the *same class node*. + return prev_cls is not None and prev_cls is curr_cls + + +def _DecoratedFuncdef(node): + return pytree_utils.DecoratedTarget(node, ('funcdef',)) diff --git a/yapf/pytree/pytree_utils.py b/yapf/pytree/pytree_utils.py index e7aa6f59c..7fa012bc1 100644 --- a/yapf/pytree/pytree_utils.py +++ b/yapf/pytree/pytree_utils.py @@ -332,3 +332,34 @@ def _PytreeNodeRepr(node): def IsCommentStatement(node): return (NodeName(node) == 'simple_stmt' and node.children[0].type == token.COMMENT) + + +def AscendTo(node, target_names): + n = node + while n is not None and NodeName(n) not in target_names: + n = getattr(n, 'parent', None) + return n if n is not None and NodeName(n) in target_names else None + + +def EnclosingFunc(node): + return node if NodeName(node) == 'funcdef' else AscendTo(node, {'funcdef'}) + + +def EnclosingClass(node): + return node if NodeName(node) == 'classdef' else AscendTo(node, {'classdef'}) + + +def IsFuncDef(node): + return node is not None and NodeName(node) == 'funcdef' + + +def IsClassDef(node): + return node is not None and NodeName(node) == 'classdef' + + +def DecoratedTarget(node, target_names=('funcdef', 'classdef')): + n = node + while n.next_sibling is not None and NodeName(n.next_sibling) == 'decorator': + n = n.next_sibling + cand = n.next_sibling + return cand if cand is not None and NodeName(cand) in target_names else None diff --git a/yapf/yapflib/style.py b/yapf/yapflib/style.py index 7642c01f4..d498801de 100644 --- a/yapf/yapflib/style.py +++ b/yapf/yapflib/style.py @@ -115,6 +115,18 @@ class Foo: def method(): pass """), + BLANK_LINES_BETWEEN_CLASS_DEFS=textwrap.dedent("""\ + Sets the number of desired blank lines between methods inside + class definitions. For example: + + class Foo: + def method1(): + pass + # <------ multiple + # <------ blank lines + def method2(): + pass + """), BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION=textwrap.dedent("""\ Number of blank lines surrounding top-level function and class definitions. @@ -486,6 +498,7 @@ def CreatePEP8Style(): BLANK_LINE_BEFORE_MODULE_DOCSTRING=False, BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF=True, BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION=2, + BLANK_LINES_BETWEEN_CLASS_DEFS=1, BLANK_LINES_BETWEEN_TOP_LEVEL_IMPORTS_AND_VARIABLES=1, COALESCE_BRACKETS=False, COLUMN_LIMIT=79, @@ -675,6 +688,7 @@ def _IntOrIntListConverter(s): BLANK_LINE_BEFORE_MODULE_DOCSTRING=_BoolConverter, BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF=_BoolConverter, BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION=int, + BLANK_LINES_BETWEEN_CLASS_DEFS=int, BLANK_LINES_BETWEEN_TOP_LEVEL_IMPORTS_AND_VARIABLES=int, COALESCE_BRACKETS=_BoolConverter, COLUMN_LIMIT=int,