Repository: google/yapf
Pull request #535 (2018-03-04 18:30:04)

Added `CONTINUATION_ALIGN_STYLE` knob to adjust how continuation lines are vertically aligned when `USE_TABS` = True
The following alignment styles are implemented:

- `SPACE`: vertically align with spaces. current (0.20.2) and default behavior.
- `FIXED`: just use fixed number of tabs (`CONTINUATION_INDENT_WIDTH` / `INDENT_WIDTH`).
- `VALIGN-RIGHT`: Vertically align continuation lines with indent characters. Slightly right (one more indent character) if cannot vertically align continuation lines with indent characters.

Here is how different styles look like:

<img width="274" alt="CONTINUATION_ALIGN_STYLE result sample" src="https://user-images.githubusercontent.com/71190/36948805-361ab606-201b-11e8-8c93-0d67e8f2d545.png">

with configuration:

```YAML
[style]
based_on_style = chromium
USE_TABS = true
COLUMN_LIMIT=32
INDENT_WIDTH=4
CONTINUATION_INDENT_WIDTH=8
# CONTINUATION_ALIGN_STYLE = space
# CONTINUATION_ALIGN_STYLE = fixed
# CONTINUATION_ALIGN_STYLE = valign-right
```

Comparing to previous PR:

* Renamed knob option name.
* Renamed `MORE` style to `VAlign-Right`.
* Removed `LESS` style. By discussion at #380 LESS style seems not useful.
* Added utility function `_ContinuationAlignStyleStringConverter()` to convert and check knob option string.
* More tests.


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 67a7440..87bc3e7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -7,6 +7,8 @@
 - Introduce a new option of formatting multiline literals. Add
   `SPLIT_BEFORE_CLOSING_BRACKET` knob to control whether closing bracket should
   get their own line.
+- Added `CONTINUATION_ALIGN_STYLE` knob to choose continuation alignment style
+  when `USE_TABS` is enabled.
 
 ## [0.20.2] 2018-02-12
 ### Changed
diff --git a/README.rst b/README.rst
index 86ab6c6..7e4bd3d 100644
--- a/README.rst
+++ b/README.rst
@@ -351,6 +351,20 @@ Knobs
 ``COLUMN_LIMIT``
     The column limit (or max line-length)
 
+``CONTINUATION_ALIGN_STYLE``
+    The style for continuation alignment. Possible values are:
+
+    - SPACE: Use spaces for continuation alignment. This is default behavior.
+    - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns
+      (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs) for continuation
+      alignment.
+    - VALIGN-RIGHT: Vertically align continuation lines with indent characters.
+      Slightly right (one more indent character) if cannot vertically align
+      continuation lines with indent characters.
+
+      For options ``FIXED``, and ``VALIGN-RIGHT`` are only available when
+      ``USE_TABS`` is enabled.
+
 ``CONTINUATION_INDENT_WIDTH``
     Indent width used for line continuations.
 
diff --git a/yapf/yapflib/format_token.py b/yapf/yapflib/format_token.py
index d86c16d..474e9b1 100644
--- a/yapf/yapflib/format_token.py
+++ b/yapf/yapflib/format_token.py
@@ -58,6 +58,28 @@ class Subtype(object):
   TYPED_NAME_ARG_LIST = 20
 
 
+def _TabbedContinuationAlignPadding(spaces, align_style, tab_width,
+                                    continuation_indent_width):
+  """Build padding string for continuation alignment in tabbed indentation.
+
+  Arguments:
+    spaces: (int) The number of spaces to place before the token for alignment.
+    align_style: (str) The alignment style for continuation lines.
+    tab_width: (int) Number of columns of each tab character.
+    continuation_indent_width: (int) Indent columns for line continuations.
+
+  Returns:
+    A padding string for alignment with style specified by align_style option.
+  """
+  if align_style == 'FIXED':
+    if spaces > 0:
+      return '\t' * int(continuation_indent_width / tab_width)
+    return ''
+  elif align_style == 'VALIGN-RIGHT':
+    return '\t' * int((spaces + tab_width - 1) / tab_width)
+  return ' ' * spaces
+
+
 class FormatToken(object):
   """A wrapper around pytree Leaf nodes.
 
@@ -123,7 +145,12 @@ class FormatToken(object):
       indent_level: (int) The indentation level.
     """
     if style.Get('USE_TABS'):
-      indent_before = '\t' * indent_level + ' ' * spaces
+      if newlines_before > 0:
+        indent_before = '\t' * indent_level + _TabbedContinuationAlignPadding(
+            spaces, style.Get('CONTINUATION_ALIGN_STYLE'),
+            style.Get('INDENT_WIDTH'), style.Get('CONTINUATION_INDENT_WIDTH'))
+      else:
+        indent_before = '\t' * indent_level + ' ' * spaces
     else:
       indent_before = (
           ' ' * indent_level * style.Get('INDENT_WIDTH') + ' ' * spaces)
diff --git a/yapf/yapflib/style.py b/yapf/yapflib/style.py
index f886537..9950f81 100644
--- a/yapf/yapflib/style.py
+++ b/yapf/yapflib/style.py
@@ -90,6 +90,21 @@ _STYLE_HELP = dict(
          })"""),
     COLUMN_LIMIT=textwrap.dedent("""\
       The column limit."""),
+    CONTINUATION_ALIGN_STYLE=textwrap.dedent("""\
+      The style for continuation alignment. Possible values are:
+
+      - SPACE: Use spaces for continuation alignment. This is default behavior.
+      - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns
+        (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs) for continuation
+        alignment.
+      - LESS: Slightly left if cannot vertically align continuation lines with
+        indent characters.
+      - VALIGN-RIGHT: Vertically align continuation lines with indent
+        characters. Slightly right (one more indent character) if cannot
+        vertically align continuation lines with indent characters.
+
+      For options FIXED, and VALIGN-RIGHT are only available when USE_TABS is
+      enabled."""),
     CONTINUATION_INDENT_WIDTH=textwrap.dedent("""\
       Indent width used for line continuations."""),
     DEDENT_CLOSING_BRACKETS=textwrap.dedent("""\
@@ -245,6 +260,7 @@ def CreatePEP8Style():
       BLANK_LINE_BEFORE_CLASS_DOCSTRING=False,
       COALESCE_BRACKETS=False,
       COLUMN_LIMIT=79,
+      CONTINUATION_ALIGN_STYLE='SPACE',
       CONTINUATION_INDENT_WIDTH=4,
       DEDENT_CLOSING_BRACKETS=False,
       EACH_DICT_ENTRY_ON_SEPARATE_LINE=True,
@@ -345,6 +361,18 @@ def _GetStyleFactory(style):
   return None
 
 
+def _ContinuationAlignStyleStringConverter(s):
+  """Option value converter for a continuation align style string."""
+  accepted_styles = ('SPACE', 'FIXED', 'VALIGN-RIGHT')
+  if s:
+    r = s.upper()
+    if r not in accepted_styles:
+      raise ValueError("unknown continuation align style: %r" % (s,))
+  else:
+    r = accepted_styles[0]
+  return r
+
+
 def _StringListConverter(s):
   """Option value converter for a comma-separated list of strings."""
   return [part.strip() for part in s.split(',')]
@@ -376,6 +404,7 @@ _STYLE_OPTION_VALUE_CONVERTER = dict(
     BLANK_LINE_BEFORE_CLASS_DOCSTRING=_BoolConverter,
     COALESCE_BRACKETS=_BoolConverter,
     COLUMN_LIMIT=int,
+    CONTINUATION_ALIGN_STYLE=_ContinuationAlignStyleStringConverter,
     CONTINUATION_INDENT_WIDTH=int,
     DEDENT_CLOSING_BRACKETS=_BoolConverter,
     EACH_DICT_ENTRY_ON_SEPARATE_LINE=_BoolConverter,


Test patch:
diff --git a/yapftests/format_token_test.py b/yapftests/format_token_test.py
index f7b613b..3adad4e 100644
--- a/yapftests/format_token_test.py
+++ b/yapftests/format_token_test.py
@@ -21,6 +21,48 @@ from lib2to3.pgen2 import token
 from yapf.yapflib import format_token
 
 
+class TabbedContinuationAlignPaddingTest(unittest.TestCase):
+
+  def testSpace(self):
+    align_style = 'SPACE'
+
+    pad = format_token._TabbedContinuationAlignPadding(0, align_style, 2, 4)
+    self.assertEqual(pad, '')
+
+    pad = format_token._TabbedContinuationAlignPadding(2, align_style, 2, 4)
+    self.assertEqual(pad, ' ' * 2)
+
+    pad = format_token._TabbedContinuationAlignPadding(5, align_style, 2, 4)
+    self.assertEqual(pad, ' ' * 5)
+
+  def testFixed(self):
+    align_style = 'FIXED'
+
+    pad = format_token._TabbedContinuationAlignPadding(0, align_style, 4, 8)
+    self.assertEqual(pad, '')
+
+    pad = format_token._TabbedContinuationAlignPadding(2, align_style, 4, 8)
+    self.assertEqual(pad, "\t" * 2)
+
+    pad = format_token._TabbedContinuationAlignPadding(5, align_style, 4, 8)
+    self.assertEqual(pad, "\t" * 2)
+
+  def testVAlignRight(self):
+    align_style = 'VALIGN-RIGHT'
+
+    pad = format_token._TabbedContinuationAlignPadding(0, align_style, 4, 8)
+    self.assertEqual(pad, '')
+
+    pad = format_token._TabbedContinuationAlignPadding(2, align_style, 4, 8)
+    self.assertEqual(pad, "\t")
+
+    pad = format_token._TabbedContinuationAlignPadding(4, align_style, 4, 8)
+    self.assertEqual(pad, "\t")
+
+    pad = format_token._TabbedContinuationAlignPadding(5, align_style, 4, 8)
+    self.assertEqual(pad, "\t" * 2)
+
+
 class FormatTokenTest(unittest.TestCase):
 
   def testSimple(self):
diff --git a/yapftests/style_test.py b/yapftests/style_test.py
index 7f9a715..694cefa 100644
--- a/yapftests/style_test.py
+++ b/yapftests/style_test.py
@@ -26,6 +26,20 @@ from yapftests import utils
 
 class UtilsTest(unittest.TestCase):
 
+  def testContinuationAlignStyleStringConverter(self):
+    self.assertEqual(style._ContinuationAlignStyleStringConverter(''), 'SPACE')
+    self.assertEqual(
+        style._ContinuationAlignStyleStringConverter('space'), 'SPACE')
+    self.assertEqual(
+        style._ContinuationAlignStyleStringConverter('fixed'), 'FIXED')
+    self.assertEqual(
+        style._ContinuationAlignStyleStringConverter('valign-right'),
+        'VALIGN-RIGHT')
+    with self.assertRaises(ValueError) as ctx:
+      style._ContinuationAlignStyleStringConverter('blahblah')
+    self.assertTrue(
+        "unknown continuation align style: 'blahblah'" in str(ctx.exception))
+
   def testStringListConverter(self):
     self.assertEqual(style._StringListConverter('foo, bar'), ['foo', 'bar'])
     self.assertEqual(style._StringListConverter('foo,bar'), ['foo', 'bar'])
diff --git a/yapftests/yapf_test.py b/yapftests/yapf_test.py
index 5f3d3fc..6df28b6 100644
--- a/yapftests/yapf_test.py
+++ b/yapftests/yapf_test.py
@@ -1196,6 +1196,62 @@ def f():
 based_on_style = chromium
 USE_TABS = true
 INDENT_WIDTH=1
+"""
+    with utils.TempFileContents(self.test_tmpdir, style_contents) as stylepath:
+      self.assertYapfReformats(
+          unformatted_code,
+          expected_formatted_code,
+          extra_options=['--style={0}'.format(stylepath)])
+
+  def testUseTabsContinuationAlignStyleFixed(self):
+    unformatted_code = """\
+def foo_function(arg1, arg2, arg3):
+  return ['hello', 'world',]
+"""
+    expected_formatted_code = """\
+def foo_function(arg1, arg2,
+		arg3):
+	return [
+			'hello',
+			'world',
+	]
+"""
+    style_contents = u"""\
+[style]
+based_on_style = chromium
+USE_TABS = true
+COLUMN_LIMIT=32
+INDENT_WIDTH=4
+CONTINUATION_INDENT_WIDTH=8
+CONTINUATION_ALIGN_STYLE = fixed
+"""
+    with utils.TempFileContents(self.test_tmpdir, style_contents) as stylepath:
+      self.assertYapfReformats(
+          unformatted_code,
+          expected_formatted_code,
+          extra_options=['--style={0}'.format(stylepath)])
+
+  def testUseTabsContinuationAlignStyleVAlignRight(self):
+    unformatted_code = """\
+def foo_function(arg1, arg2, arg3):
+  return ['hello', 'world',]
+"""
+    expected_formatted_code = """\
+def foo_function(arg1, arg2,
+					arg3):
+	return [
+			'hello',
+			'world',
+	]
+"""
+    style_contents = u"""\
+[style]
+based_on_style = chromium
+USE_TABS = true
+COLUMN_LIMIT=32
+INDENT_WIDTH=4
+CONTINUATION_INDENT_WIDTH=8
+CONTINUATION_ALIGN_STYLE = valign-right
 """
     with utils.TempFileContents(self.test_tmpdir, style_contents) as stylepath:
       self.assertYapfReformats(