Repository: google/yapf
Pull request #780 (2019-11-14 18:42:59)

add INDENT_CLOSING_BRACKETS option
unlike CLOSING_INDENT_BRACKETS, this option leaves the final bracket
aligned with the previous line's first column


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 1f3002d..49ad995 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,6 +6,9 @@
 ### Added
 - Add the `--quiet` flag to suppress output. The return code is 1 if there are
   changes, similarly to the `--diff` flag.
+- Add the `indent_closing_brackets` option. This is the same as the
+  `dedent_closing_brackets` option except the brackets are indented the same
+  as the previous line.
 ### Changed
 - Collect a parameter list into a single object. This allows us to track how a
   parameter list is formatted, keeping state along the way. This helps when
diff --git a/README.rst b/README.rst
index 6ab3132..0d86e3d 100644
--- a/README.rst
+++ b/README.rst
@@ -394,7 +394,8 @@ Knobs
 
 ``COALESCE_BRACKETS``
     Do not split consecutive brackets. Only relevant when
-    ``DEDENT_CLOSING_BRACKETS`` is set. For example:
+    ``DEDENT_CLOSING_BRACKETS`` or ``INDENT_CLOSING_BRACKETS``
+    is set. For example:
 
     .. code-block:: python
 
@@ -492,6 +493,26 @@ Knobs
 ``INDENT_BLANK_LINES``
     Set to ``True`` to prefer indented blank lines rather than empty
 
+``INDENT_CLOSING_BRACKETS``
+    Put closing brackets on a separate line, indented, if the bracketed
+    expression can't fit in a single line. Applies to all kinds of brackets,
+    including function definitions and calls. For example:
+
+    .. code-block:: python
+
+        config = {
+            'key1': 'value1',
+            'key2': 'value2',
+            }  # <--- this bracket is indented and on a separate line
+
+        time_series = self.remote_client.query_entity_counters(
+            entity='dev3246.region1',
+            key='dns.query_latency_tcp',
+            transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
+            start_ts=now()-timedelta(days=3),
+            end_ts=now(),
+            )  # <--- this bracket is indented and on a separate line
+
 ``JOIN_MULTIPLE_LINES``
     Join short lines into one line. E.g., single line ``if`` statements.
 
diff --git a/yapf/yapflib/format_decision_state.py b/yapf/yapflib/format_decision_state.py
index 3fac458..04ed911 100644
--- a/yapf/yapflib/format_decision_state.py
+++ b/yapf/yapflib/format_decision_state.py
@@ -195,7 +195,8 @@ class FormatDecisionState(object):
       return not self._ContainerFitsOnStartLine(opening)
 
     if (self.stack[-1].split_before_closing_bracket and
-        current.value in '}]' and style.Get('SPLIT_BEFORE_CLOSING_BRACKET')):
+        (current.value in '}]' and style.Get('SPLIT_BEFORE_CLOSING_BRACKET') or
+         current.value in '}])' and style.Get('INDENT_CLOSING_BRACKETS'))):
       # Split before the closing bracket if we can.
       if format_token.Subtype.SUBSCRIPT_BRACKET not in current.subtypes:
         return current.node_split_penalty != split_penalty.UNBREAKABLE
@@ -214,6 +215,7 @@ class FormatDecisionState(object):
     ###########################################################################
     # List Splitting
     if (style.Get('DEDENT_CLOSING_BRACKETS') or
+        style.Get('INDENT_CLOSING_BRACKETS') or
         style.Get('SPLIT_BEFORE_FIRST_ARGUMENT')):
       bracket = current if current.ClosesScope() else previous
       if format_token.Subtype.SUBSCRIPT_BRACKET not in bracket.subtypes:
@@ -235,7 +237,8 @@ class FormatDecisionState(object):
             self.stack[-1].split_before_closing_bracket = True
             return True
 
-        elif style.Get('DEDENT_CLOSING_BRACKETS') and current.ClosesScope():
+        elif (style.Get('DEDENT_CLOSING_BRACKETS') or
+              style.Get('INDENT_CLOSING_BRACKETS')) and current.ClosesScope():
           # Split before and dedent the closing bracket.
           return self.stack[-1].split_before_closing_bracket
 
@@ -649,9 +652,10 @@ class FormatDecisionState(object):
     if (previous.OpensScope() or
         (previous.is_comment and previous.previous_token is not None and
          previous.previous_token.OpensScope())):
-      self.stack[-1].closing_scope_indent = max(
-          0, self.stack[-1].indent - style.Get('CONTINUATION_INDENT_WIDTH'))
-
+      dedent = (style.Get('CONTINUATION_INDENT_WIDTH'),
+                0)[style.Get('INDENT_CLOSING_BRACKETS')]
+      self.stack[-1].closing_scope_indent = max(0,
+                                                self.stack[-1].indent - dedent)
       self.stack[-1].split_before_closing_bracket = True
 
     # Calculate the split penalty.
@@ -942,7 +946,8 @@ class FormatDecisionState(object):
           return top_of_stack.indent
 
     if (_IsCompoundStatement(self.line.first) and
-        (not style.Get('DEDENT_CLOSING_BRACKETS') or
+        (not (style.Get('DEDENT_CLOSING_BRACKETS') or
+              style.Get('INDENT_CLOSING_BRACKETS')) or
          style.Get('SPLIT_BEFORE_FIRST_ARGUMENT'))):
       token_indent = (
           len(self.line.first.whitespace_prefix.split('\n')[-1]) +
diff --git a/yapf/yapflib/split_penalty.py b/yapf/yapflib/split_penalty.py
index e15ea2e..958c76e 100644
--- a/yapf/yapflib/split_penalty.py
+++ b/yapf/yapflib/split_penalty.py
@@ -139,7 +139,8 @@ class _SplitPenaltyAssigner(pytree_visitor.PyTreeVisitor):
 
     # Can't break before the opening paren of a parameter list.
     _SetUnbreakable(node.children[0])
-    if not style.Get('DEDENT_CLOSING_BRACKETS'):
+    if not (style.Get('INDENT_CLOSING_BRACKETS') or
+            style.Get('DEDENT_CLOSING_BRACKETS')):
       _SetStronglyConnected(node.children[-1])
 
   def Visit_arglist(self, node):  # pylint: disable=invalid-name
@@ -321,7 +322,8 @@ class _SplitPenaltyAssigner(pytree_visitor.PyTreeVisitor):
           last_child_node = pytree_utils.LastLeafNode(trailer)
           if last_child_node.value.strip().startswith('#'):
             last_child_node = last_child_node.prev_sibling
-          if not style.Get('DEDENT_CLOSING_BRACKETS'):
+          if not (style.Get('INDENT_CLOSING_BRACKETS') or
+                  style.Get('DEDENT_CLOSING_BRACKETS')):
             last = pytree_utils.LastLeafNode(last_child_node.prev_sibling)
             if last.value != ',':
               if last_child_node.value == ']':
diff --git a/yapf/yapflib/style.py b/yapf/yapflib/style.py
index e614e62..77c9172 100644
--- a/yapf/yapflib/style.py
+++ b/yapf/yapflib/style.py
@@ -150,6 +150,23 @@ _STYLE_HELP = dict(
             start_ts=now()-timedelta(days=3),
             end_ts=now(),
         )        # <--- this bracket is dedented and on a separate line"""),
+    INDENT_CLOSING_BRACKETS=textwrap.dedent("""\
+      Put closing brackets on a separate line, indented, if the bracketed
+      expression can't fit in a single line. Applies to all kinds of brackets,
+      including function definitions and calls. For example:
+
+        config = {
+            'key1': 'value1',
+            'key2': 'value2',
+            }        # <--- this bracket is indented and on a separate line
+
+        time_series = self.remote_client.query_entity_counters(
+            entity='dev3246.region1',
+            key='dns.query_latency_tcp',
+            transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
+            start_ts=now()-timedelta(days=3),
+            end_ts=now(),
+            )        # <--- this bracket is indented and on a separate line"""),
     DISABLE_ENDING_COMMA_HEURISTIC=textwrap.dedent("""\
       Disable the heuristic which places each list element on a separate line
       if the list is comma-terminated."""),
@@ -355,6 +372,7 @@ def CreatePEP8Style():
       CONTINUATION_ALIGN_STYLE='SPACE',
       CONTINUATION_INDENT_WIDTH=4,
       DEDENT_CLOSING_BRACKETS=False,
+      INDENT_CLOSING_BRACKETS=False,
       DISABLE_ENDING_COMMA_HEURISTIC=False,
       EACH_DICT_ENTRY_ON_SEPARATE_LINE=True,
       I18N_COMMENT='',
@@ -431,6 +449,7 @@ def CreateFacebookStyle():
   style['ALIGN_CLOSING_BRACKET_WITH_VISUAL_INDENT'] = False
   style['COLUMN_LIMIT'] = 80
   style['DEDENT_CLOSING_BRACKETS'] = True
+  style['INDENT_CLOSING_BRACKETS'] = False
   style['INDENT_DICTIONARY_VALUE'] = True
   style['JOIN_MULTIPLE_LINES'] = False
   style['SPACES_BEFORE_COMMENT'] = 2
@@ -532,6 +551,7 @@ _STYLE_OPTION_VALUE_CONVERTER = dict(
     CONTINUATION_ALIGN_STYLE=_ContinuationAlignStyleStringConverter,
     CONTINUATION_INDENT_WIDTH=int,
     DEDENT_CLOSING_BRACKETS=_BoolConverter,
+    INDENT_CLOSING_BRACKETS=_BoolConverter,
     DISABLE_ENDING_COMMA_HEURISTIC=_BoolConverter,
     EACH_DICT_ENTRY_ON_SEPARATE_LINE=_BoolConverter,
     I18N_COMMENT=str,


Test patch:
diff --git a/yapftests/reformatter_basic_test.py b/yapftests/reformatter_basic_test.py
index be3fbbc..bbceb21 100644
--- a/yapftests/reformatter_basic_test.py
+++ b/yapftests/reformatter_basic_test.py
@@ -2710,6 +2710,175 @@ x = [1, 2, 3, 4, 5, 6, 7,]
     finally:
       style.SetGlobalStyle(style.CreateChromiumStyle())
 
+  def testIndentClosingBracketsWithTypeAnnotationExceedingLineLength(self):
+    try:
+      style.SetGlobalStyle(
+          style.CreateStyleFromConfig('{based_on_style: chromium,'
+                                      ' indent_closing_brackets: True}'))
+      unformatted_code = textwrap.dedent("""\
+                def function(first_argument_xxxxxxxxxxxxxxxx=(0,), second_argument=None) -> None:
+                  pass
+
+
+                def function(first_argument_xxxxxxxxxxxxxxxxxxxxxxx=(0,), second_argument=None) -> None:
+                  pass
+                """)
+      expected_formatted_code = textwrap.dedent("""\
+                def function(
+                    first_argument_xxxxxxxxxxxxxxxx=(0,), second_argument=None
+                    ) -> None:
+                  pass
+
+
+                def function(
+                    first_argument_xxxxxxxxxxxxxxxxxxxxxxx=(0,), second_argument=None
+                    ) -> None:
+                  pass
+                """)
+      uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
+      self.assertCodeEqual(expected_formatted_code,
+                           reformatter.Reformat(uwlines))
+    finally:
+      style.SetGlobalStyle(style.CreateChromiumStyle())
+
+  def testIndentClosingBracketsInFunctionCall(self):
+    try:
+      style.SetGlobalStyle(
+          style.CreateStyleFromConfig('{based_on_style: chromium,'
+                                      ' indent_closing_brackets: True}'))
+      unformatted_code = textwrap.dedent("""\
+                def function(first_argument_xxxxxxxxxxxxxxxx=(0,), second_argument=None, third_and_final_argument=True):
+                  pass
+
+
+                def function(first_argument_xxxxxxxxxxxxxxxxxxxxxxx=(0,), second_and_last_argument=None):
+                  pass
+                """)
+      expected_formatted_code = textwrap.dedent("""\
+                def function(
+                    first_argument_xxxxxxxxxxxxxxxx=(0,),
+                    second_argument=None,
+                    third_and_final_argument=True
+                    ):
+                  pass
+
+
+                def function(
+                    first_argument_xxxxxxxxxxxxxxxxxxxxxxx=(0,), second_and_last_argument=None
+                    ):
+                  pass
+                """)
+      uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
+      self.assertCodeEqual(expected_formatted_code,
+                           reformatter.Reformat(uwlines))
+    finally:
+      style.SetGlobalStyle(style.CreateChromiumStyle())
+
+  def testIndentClosingBracketsInTuple(self):
+    try:
+      style.SetGlobalStyle(
+          style.CreateStyleFromConfig('{based_on_style: chromium,'
+                                      ' indent_closing_brackets: True}'))
+      unformatted_code = textwrap.dedent("""\
+                def function():
+                  some_var = ('a long element', 'another long element', 'short element', 'really really long element')
+                  return True
+
+                def function():
+                  some_var = ('a couple', 'small', 'elemens')
+                  return False
+                """)
+      expected_formatted_code = textwrap.dedent("""\
+                def function():
+                  some_var = (
+                      'a long element', 'another long element', 'short element',
+                      'really really long element'
+                      )
+                  return True
+
+
+                def function():
+                  some_var = ('a couple', 'small', 'elemens')
+                  return False
+                """)
+      uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
+      self.assertCodeEqual(expected_formatted_code,
+                           reformatter.Reformat(uwlines))
+    finally:
+      style.SetGlobalStyle(style.CreateChromiumStyle())
+
+  def testIndentClosingBracketsInList(self):
+    try:
+      style.SetGlobalStyle(
+          style.CreateStyleFromConfig('{based_on_style: chromium,'
+                                      ' indent_closing_brackets: True}'))
+      unformatted_code = textwrap.dedent("""\
+                def function():
+                  some_var = ['a long element', 'another long element', 'short element', 'really really long element']
+                  return True
+
+                def function():
+                  some_var = ['a couple', 'small', 'elemens']
+                  return False
+                """)
+      expected_formatted_code = textwrap.dedent("""\
+                def function():
+                  some_var = [
+                      'a long element', 'another long element', 'short element',
+                      'really really long element'
+                      ]
+                  return True
+
+
+                def function():
+                  some_var = ['a couple', 'small', 'elemens']
+                  return False
+                """)
+      uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
+      self.assertCodeEqual(expected_formatted_code,
+                           reformatter.Reformat(uwlines))
+    finally:
+      style.SetGlobalStyle(style.CreateChromiumStyle())
+
+  def testIndentClosingBracketsInDict(self):
+    try:
+      style.SetGlobalStyle(
+          style.CreateStyleFromConfig('{based_on_style: chromium,'
+                                      ' indent_closing_brackets: True}'))
+      unformatted_code = textwrap.dedent("""\
+                def function():
+                  some_var = {1: ('a long element', 'and another really really long element that is really really amazingly long'), 2: 'another long element', 3: 'short element', 4: 'really really long element'}
+                  return True
+
+                def function():
+                  some_var = {1: 'a couple', 2: 'small', 3: 'elemens'}
+                  return False
+                """)
+      expected_formatted_code = textwrap.dedent("""\
+                def function():
+                  some_var = {
+                      1:
+                          (
+                              'a long element',
+                              'and another really really long element that is really really amazingly long'
+                              ),
+                      2: 'another long element',
+                      3: 'short element',
+                      4: 'really really long element'
+                      }
+                  return True
+
+
+                def function():
+                  some_var = {1: 'a couple', 2: 'small', 3: 'elemens'}
+                  return False
+                """)
+      uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
+      self.assertCodeEqual(expected_formatted_code,
+                           reformatter.Reformat(uwlines))
+    finally:
+      style.SetGlobalStyle(style.CreateChromiumStyle())
+
   def testMultipleDictionariesInList(self):
     unformatted_code = """\
 class A: