Repository: google/yapf

Issue:
`split_before_first_argument` doesn't work on some cases
There seem to be some corner cases where `split_before_first_argument` doesn't seem to work as expected (at least not as I'd expect). For example using this style config

```
[style]
based_on_style = pep8
align_closing_bracket_with_visual_indent=True
coalesce_brackets=True
dedent_closing_brackets=False
indent_dictionary_value=True
spaces_around_power_operator=False
space_between_ending_comma_and_closing_bracket=False
split_arguments_when_comma_terminated=True
split_before_first_argument = False
split_before_named_assigns = False
split_penalty_after_opening_bracket=30
split_penalty_for_added_line_split=30
use_tabs=False
```

the following unformatted code:

```
a_super_long_version_of_print(argument1, argument2, argument3, argument4, argument5, argument6, argument7)
CREDS_FILE = os.path.join(os.path.expanduser('~'), 'apis/super-secret-admin-creds.json')
```

Gets formatted into:

```
a_super_long_version_of_print(argument1, argument2, argument3, argument4,
                              argument5, argument6, argument7)
CREDS_FILE = os.path.join(
    os.path.expanduser('~'), 'apis/super-secret-admin-creds.json')
```

While the formatted original first line looks the way I'd expect, with all the arguments it can fit in the first line, and the remaining ones hanging at the same indentation level as the first one, the second original line seems to apply a different heuristic. Instead of what I'm getting, I was expecting to get:

```
CREDS_FILE = os.path.join(os.path.expanduser('~'),
                          'apis/super-secret-admin-creds.json')
```

If this is the expected behaviour, I'd appreciate if you can let me know if there's a configuration knob I could use to achieve my desired style. 

Comments:

Patch:
diff --git a/CHANGELOG b/CHANGELOG
index ee72fdb..1df010a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -11,6 +11,9 @@
 - Parse integer lists correctly, removing quotes if the list is within a
   string.
 - Adjust the penalties of bitwise operands for '&' and '^', similar to '|'.
+- Avoid splitting after opening parens if SPLIT_BEFORE_FIRST_ARGUMENT is set
+  to False.
+- Adjust default SPLIT_PENALTY_AFTER_OPENING_BRACKET.
 
 ## [0.26.0] 2019-02-08
 ### Added
@@ -20,7 +23,7 @@
   if they have higher precedence than other operators in the same expression.
 ### Changed
 - `SPACES_BEFORE_COMMENT` can now be assigned to a specific value (standard
-  behavior) or a list of column values. When assigned to a list, trailing 
+  behavior) or a list of column values. When assigned to a list, trailing
   comments will be horizontally aligned to the first column value within
   the list that is greater than the maximum line length in the block.
 - Don't modify the vertical spacing of a line that has a comment "pylint:
diff --git a/yapf/__init__.py b/yapf/__init__.py
index 65584f8..97a9633 100644
--- a/yapf/__init__.py
+++ b/yapf/__init__.py
@@ -195,9 +195,9 @@ def main(argv):
   exclude_patterns_from_ignore_file = file_resources.GetExcludePatternsForDir(
       os.getcwd())
 
-  files = file_resources.GetCommandLineFiles(
-      args.files, args.recursive,
-      (args.exclude or []) + exclude_patterns_from_ignore_file)
+  files = file_resources.GetCommandLineFiles(args.files, args.recursive,
+                                             (args.exclude or []) +
+                                             exclude_patterns_from_ignore_file)
   if not files:
     raise errors.YapfError('Input filenames did not match any python files')
 
diff --git a/yapf/yapflib/format_decision_state.py b/yapf/yapflib/format_decision_state.py
index 8dd5e55..486be8b 100644
--- a/yapf/yapflib/format_decision_state.py
+++ b/yapf/yapflib/format_decision_state.py
@@ -359,6 +359,11 @@ class FormatDecisionState(object):
             # assigns.
             return False
 
+          # Don't split if not required
+          if (not style.Get('SPLIT_BEFORE_EXPRESSION_AFTER_OPENING_PAREN') and
+              not style.Get('SPLIT_BEFORE_FIRST_ARGUMENT')):
+            return False
+
           column = self.column - self.stack[-1].last_space
           return column > style.Get('CONTINUATION_INDENT_WIDTH')
 
@@ -412,12 +417,14 @@ class FormatDecisionState(object):
     pprevious = previous.previous_token
     if (current.is_name and pprevious and pprevious.is_name and
         previous.value == '('):
+
       if (not self._FitsOnLine(previous, previous.matching_bracket) and
           _IsFunctionCallWithArguments(current)):
         # There is a function call, with more than 1 argument, where the first
-        # argument is itself a function call with arguments.  In this specific
-        # case, if we split after the first argument's opening '(', then the
-        # formatting will look bad for the rest of the arguments. E.g.:
+        # argument is itself a function call with arguments that does not fit
+        # into the line.  In this specific case, if we split after the first
+        # argument's opening '(', then the formatting will look bad for the
+        # rest of the arguments. E.g.:
         #
         #     outer_function_call(inner_function_call(
         #         inner_arg1, inner_arg2),
@@ -425,7 +432,31 @@ class FormatDecisionState(object):
         #
         # Instead, enforce a split before that argument to keep things looking
         # good.
-        return True
+        if (style.Get('SPLIT_BEFORE_EXPRESSION_AFTER_OPENING_PAREN') or
+            style.Get('SPLIT_BEFORE_FIRST_ARGUMENT')):
+          return True
+
+        opening = _GetOpeningBracket(current)
+        if (opening and opening.value == '(' and opening.previous_token and
+            (opening.previous_token.is_name or
+             opening.previous_token.value in {'*', '**'})):
+          is_func_call = False
+          opening = current
+          while opening:
+            if opening.value == '(':
+              is_func_call = True
+              break
+            if (not (opening.is_name or opening.value in {'*', '**'}) and
+                opening.value != '.'):
+              break
+            opening = opening.next_token
+
+          if is_func_call:
+            if (not self._FitsOnLine(current, opening.matching_bracket) or
+                (opening.matching_bracket.next_token and
+                 opening.matching_bracket.next_token.value != ',' and
+                 not opening.matching_bracket.next_token.ClosesScope())):
+              return True
 
     if (previous.OpensScope() and not current.OpensScope() and
         not current.is_comment and
diff --git a/yapf/yapflib/reformatter.py b/yapf/yapflib/reformatter.py
index b7c7491..48858a3 100644
--- a/yapf/yapflib/reformatter.py
+++ b/yapf/yapflib/reformatter.py
@@ -660,8 +660,9 @@ def _CalculateNumberOfNewlines(first_token, indent_depth, prev_uwline,
             prev_last_token.AdjustNewlinesBefore(
                 1 + style.Get('BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION'))
           if first_token.newlines is not None:
-            pytree_utils.SetNodeAnnotation(
-                first_token.node, pytree_utils.Annotation.NEWLINES, None)
+            pytree_utils.SetNodeAnnotation(first_token.node,
+                                           pytree_utils.Annotation.NEWLINES,
+                                           None)
           return NO_BLANK_LINES
     elif _IsClassOrDef(prev_uwline):
       if not style.Get('BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF'):
diff --git a/yapf/yapflib/style.py b/yapf/yapflib/style.py
index c94da3c..359246e 100644
--- a/yapf/yapflib/style.py
+++ b/yapf/yapflib/style.py
@@ -202,7 +202,7 @@ _STYLE_HELP = dict(
       alignment column values; trailing comments within a block will
       be aligned to the first column value that is greater than the maximum
       line length within the block). For example:
-      
+
       With spaces_before_comment=5:
 
         1 + 1 # Adding values
@@ -377,7 +377,7 @@ def CreatePEP8Style():
       SPLIT_BEFORE_LOGICAL_OPERATOR=True,
       SPLIT_BEFORE_NAMED_ASSIGNS=True,
       SPLIT_COMPLEX_COMPREHENSION=False,
-      SPLIT_PENALTY_AFTER_OPENING_BRACKET=30,
+      SPLIT_PENALTY_AFTER_OPENING_BRACKET=300,
       SPLIT_PENALTY_AFTER_UNARY_OPERATOR=10000,
       SPLIT_PENALTY_ARITHMETIC_OPERATOR=300,
       SPLIT_PENALTY_BEFORE_IF_EXPR=0,


Test patch:
diff --git a/yapftests/reformatter_buganizer_test.py b/yapftests/reformatter_buganizer_test.py
index 22309ac..4d555e3 100644
--- a/yapftests/reformatter_buganizer_test.py
+++ b/yapftests/reformatter_buganizer_test.py
@@ -1546,7 +1546,7 @@ class _():
     try:
       style.SetGlobalStyle(
           style.CreateStyleFromConfig(
-              '{based_on_style: pep8, split_penalty_import_names: 35}'))
+              '{based_on_style: pep8, split_penalty_import_names: 350}'))
       unformatted_code = textwrap.dedent("""\
           from a_very_long_or_indented_module_name_yada_yada import (long_argument_1,
                                                                      long_argument_2)
@@ -2193,8 +2193,8 @@ dddddddddddddddddd().eeeeeeeeeeeeeeeeeeeee().fffffffffffffffff().ggggggggggggggg
             })
         """)
     expected_formatted_code = textwrap.dedent("""\
-        shelf_renderer.expand_text = text.translate_to_unicode(
-            expand_text % {'creator': creator})
+        shelf_renderer.expand_text = text.translate_to_unicode(expand_text %
+                                                               {'creator': creator})
         """)
     uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
     self.assertCodeEqual(expected_formatted_code, reformatter.Reformat(uwlines))
diff --git a/yapftests/reformatter_pep8_test.py b/yapftests/reformatter_pep8_test.py
index 729b33c..05d2cf0 100644
--- a/yapftests/reformatter_pep8_test.py
+++ b/yapftests/reformatter_pep8_test.py
@@ -190,8 +190,10 @@ class TestsForPEP8Style(yapf_test_helper.YAPFTest):
         if (aaaaaaaaaaaaaa + bbbbbbbbbbbbbbbb == ccccccccccccccccc and xxxxxxxxxxxxx
                 or yyyyyyyyyyyyyyyyy):
             pass
-        elif (xxxxxxxxxxxxxxx(
-                aaaaaaaaaaa, bbbbbbbbbbbbbb, cccccccccccc, dddddddddd=None)):
+        elif (xxxxxxxxxxxxxxx(aaaaaaaaaaa,
+                              bbbbbbbbbbbbbb,
+                              cccccccccccc,
+                              dddddddddd=None)):
             pass
 
 
diff --git a/yapftests/reformatter_style_config_test.py b/yapftests/reformatter_style_config_test.py
index a369279..5fe9709 100644
--- a/yapftests/reformatter_style_config_test.py
+++ b/yapftests/reformatter_style_config_test.py
@@ -121,6 +121,78 @@ class TestsForStyleConfig(yapf_test_helper.YAPFTest):
       style.SetGlobalStyle(style.CreatePEP8Style())
       style.DEFAULT_STYLE = self.current_style
 
+  def testNoSplitBeforeFirstArgumentStyle1(self):
+    try:
+      pep8_no_split_before_first = style.CreatePEP8Style()
+      pep8_no_split_before_first['SPLIT_BEFORE_FIRST_ARGUMENT'] = False
+      pep8_no_split_before_first['SPLIT_BEFORE_NAMED_ASSIGNS'] = False
+      style.SetGlobalStyle(pep8_no_split_before_first)
+      formatted_code = textwrap.dedent("""\
+          # Example from in-code MustSplit comments
+          foo = outer_function_call(fitting_inner_function_call(inner_arg1, inner_arg2),
+                                    outer_arg1, outer_arg2)
+
+          foo = outer_function_call(
+              not_fitting_inner_function_call(inner_arg1, inner_arg2), outer_arg1,
+              outer_arg2)
+
+          # Examples Issue#424
+          a_super_long_version_of_print(argument1, argument2, argument3, argument4,
+                                        argument5, argument6, argument7)
+
+          CREDS_FILE = os.path.join(os.path.expanduser('~'),
+                                    'apis/super-secret-admin-creds.json')
+
+          # Examples Issue#556
+          i_take_a_lot_of_params(arg1, param1=very_long_expression1(),
+                                 param2=very_long_expression2(),
+                                 param3=very_long_expression3(),
+                                 param4=very_long_expression4())
+
+          # Examples Issue#590
+          plt.plot(numpy.linspace(0, 1, 10), numpy.linspace(0, 1, 10), marker="x",
+                   color="r")
+
+          plt.plot(veryverylongvariablename, veryverylongvariablename, marker="x",
+                   color="r")
+          """)
+      uwlines = yapf_test_helper.ParseAndUnwrap(formatted_code)
+      self.assertCodeEqual(formatted_code, reformatter.Reformat(uwlines))
+    finally:
+      style.SetGlobalStyle(style.CreatePEP8Style())
+      style.DEFAULT_STYLE = self.current_style
+
+  def testNoSplitBeforeFirstArgumentStyle2(self):
+    try:
+      pep8_no_split_before_first = style.CreatePEP8Style()
+      pep8_no_split_before_first['SPLIT_BEFORE_FIRST_ARGUMENT'] = False
+      pep8_no_split_before_first['SPLIT_BEFORE_NAMED_ASSIGNS'] = True
+      style.SetGlobalStyle(pep8_no_split_before_first)
+      formatted_code = textwrap.dedent("""\
+          # Examples Issue#556
+          i_take_a_lot_of_params(arg1,
+                                 param1=very_long_expression1(),
+                                 param2=very_long_expression2(),
+                                 param3=very_long_expression3(),
+                                 param4=very_long_expression4())
+
+          # Examples Issue#590
+          plt.plot(numpy.linspace(0, 1, 10),
+                   numpy.linspace(0, 1, 10),
+                   marker="x",
+                   color="r")
+
+          plt.plot(veryverylongvariablename,
+                   veryverylongvariablename,
+                   marker="x",
+                   color="r")
+          """)
+      uwlines = yapf_test_helper.ParseAndUnwrap(formatted_code)
+      self.assertCodeEqual(formatted_code, reformatter.Reformat(uwlines))
+    finally:
+      style.SetGlobalStyle(style.CreatePEP8Style())
+      style.DEFAULT_STYLE = self.current_style
+
 
 if __name__ == '__main__':
   unittest.main()