Repository: google/yapf
Pull request #428 (2017-07-21 12:53:52)

Make yapf put the correct number of blank lines at --lines boundaries.
This allows YAPF to have the correct line counts in most cases. Namely
lines can be deleted only if they are in the range, but if either side
of a range needs correcting of blank lines more lines can be inserted to
get to the correct number.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/12493599/badge)](https://coveralls.io/builds/12493599)

Coverage increased (+0.2%) to 96.042% when pulling **63b8ad18b43bee2ebe47d09f6c39fabc761d0938 on kevincox:blank-lines-range** into **4aa43d8b7119bcd03d8b7585bb7ff7a774222ee7 on google:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/12493599/badge)](https://coveralls.io/builds/12493599)

Coverage increased (+0.2%) to 96.042% when pulling **63b8ad18b43bee2ebe47d09f6c39fabc761d0938 on kevincox:blank-lines-range** into **4aa43d8b7119bcd03d8b7585bb7ff7a774222ee7 on google:master**.

gwelymernans: Thanks! And nice cleanup of `_MarkLinesToFormat`! Could you update the `CHANGELOG` file as well?
kevincox: I couldn't quite figure out the CHANGELOG protocol so let me know if that works.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/12499453/badge)](https://coveralls.io/builds/12499453)

Coverage increased (+0.2%) to 96.042% when pulling **d839b8dcd917a92750fab6cf146e911a33105fad on kevincox:blank-lines-range** into **4aa43d8b7119bcd03d8b7585bb7ff7a774222ee7 on google:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/12499460/badge)](https://coveralls.io/builds/12499460)

Coverage increased (+0.2%) to 96.042% when pulling **d839b8dcd917a92750fab6cf146e911a33105fad on kevincox:blank-lines-range** into **4aa43d8b7119bcd03d8b7585bb7ff7a774222ee7 on google:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/12499460/badge)](https://coveralls.io/builds/12499460)

Coverage increased (+0.2%) to 96.042% when pulling **d839b8dcd917a92750fab6cf146e911a33105fad on kevincox:blank-lines-range** into **4aa43d8b7119bcd03d8b7585bb7ff7a774222ee7 on google:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/12499460/badge)](https://coveralls.io/builds/12499460)

Coverage increased (+0.2%) to 96.042% when pulling **d839b8dcd917a92750fab6cf146e911a33105fad on kevincox:blank-lines-range** into **4aa43d8b7119bcd03d8b7585bb7ff7a774222ee7 on google:master**.


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index f54ccfd..3a3ba8f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,6 +2,10 @@
 # All notable changes to this project will be documented in this file.
 # This project adheres to [Semantic Versioning](http://semver.org/).
 
+## [0.16.4] Unreleased
+### Changed
+- Adjust blank lines on formatting boundaries when using the `--lines` option.
+
 ## [0.16.3] 2017-07-13
 ### Changed
 - Add filename information to a ParseError excetion.
diff --git a/yapf/yapflib/reformatter.py b/yapf/yapflib/reformatter.py
index 1235abd..2f0cc43 100644
--- a/yapf/yapflib/reformatter.py
+++ b/yapf/yapflib/reformatter.py
@@ -36,12 +36,14 @@ from yapf.yapflib import style
 from yapf.yapflib import verifier
 
 
-def Reformat(uwlines, verify=False):
+def Reformat(uwlines, verify=False, lines=None):
   """Reformat the unwrapped lines.
 
   Arguments:
     uwlines: (list of unwrapped_line.UnwrappedLine) Lines we want to format.
     verify: (bool) True if reformatted code should be verified for syntax.
+    lines: (set of int) The lines which can be modified or None if there is no
+      line range restriction.
 
   Returns:
     A string representing the reformatted code.
@@ -66,14 +68,15 @@ def Reformat(uwlines, verify=False):
       if prev_uwline and prev_uwline.disable:
         # Keep the vertical spacing between a disabled and enabled formatting
         # region.
-        _RetainVerticalSpacingBetweenTokens(uwline.first, prev_uwline.last)
+        _RetainRequiredVerticalSpacingBetweenTokens(
+            uwline.first, prev_uwline.last, lines)
       if any(tok.is_comment for tok in uwline.tokens):
         _RetainVerticalSpacingBeforeComments(uwline)
 
     if (_LineContainsI18n(uwline) or uwline.disable or
         _LineHasContinuationMarkers(uwline)):
       _RetainHorizontalSpacing(uwline)
-      _RetainVerticalSpacing(uwline, prev_uwline)
+      _RetainRequiredVerticalSpacing(uwline, prev_uwline, lines)
       _EmitLineUnformatted(state)
     elif _CanPlaceOnSingleLine(uwline) and not any(tok.must_split
                                                    for tok in uwline.tokens):
@@ -87,7 +90,7 @@ def Reformat(uwlines, verify=False):
         state = format_decision_state.FormatDecisionState(uwline, indent_amt)
         state.MoveStateToNextToken()
         _RetainHorizontalSpacing(uwline)
-        _RetainVerticalSpacing(uwline, prev_uwline)
+        _RetainRequiredVerticalSpacing(uwline, prev_uwline, None)
         _EmitLineUnformatted(state)
 
     final_lines.append(uwline)
@@ -101,17 +104,17 @@ def _RetainHorizontalSpacing(uwline):
     tok.RetainHorizontalSpacing(uwline.first.column, uwline.depth)
 
 
-def _RetainVerticalSpacing(cur_uwline, prev_uwline):
+def _RetainRequiredVerticalSpacing(cur_uwline, prev_uwline, lines):
   prev_tok = None
   if prev_uwline is not None:
     prev_tok = prev_uwline.last
   for cur_tok in cur_uwline.tokens:
-    _RetainVerticalSpacingBetweenTokens(cur_tok, prev_tok)
+    _RetainRequiredVerticalSpacingBetweenTokens(cur_tok, prev_tok, lines)
     prev_tok = cur_tok
 
 
-def _RetainVerticalSpacingBetweenTokens(cur_tok, prev_tok):
-  """Retain vertical spacing between two tokens."""
+def _RetainRequiredVerticalSpacingBetweenTokens(cur_tok, prev_tok, lines):
+  """Retain vertical spacing between two tokens if not in editable range."""
   if prev_tok is None:
     return
 
@@ -133,7 +136,16 @@ def _RetainVerticalSpacingBetweenTokens(cur_tok, prev_tok):
   if prev_tok.value.endswith('\\'):
     prev_lineno = prev_lineno + prev_tok.value.count('\n')
 
-  cur_tok.AdjustNewlinesBefore(cur_lineno - prev_lineno)
+  required_newlines = cur_lineno - prev_lineno
+
+  if lines and (cur_lineno in lines or prev_lineno in lines):
+    desired_newlines = cur_tok.whitespace_prefix.count('\n')
+    whitespace_lines = range(prev_lineno + 1, cur_lineno)
+    deletable_lines = len(lines.intersection(whitespace_lines))
+    required_newlines = max(required_newlines - deletable_lines,
+                            desired_newlines)
+
+  cur_tok.AdjustNewlinesBefore(required_newlines)
 
 
 def _RetainVerticalSpacingBeforeComments(uwline):
diff --git a/yapf/yapflib/yapf_api.py b/yapf/yapflib/yapf_api.py
index f0922c2..cb10e8b 100644
--- a/yapf/yapflib/yapf_api.py
+++ b/yapf/yapflib/yapf_api.py
@@ -141,8 +141,9 @@ def FormatCode(unformatted_source,
   for uwl in uwlines:
     uwl.CalculateFormattingInformation()
 
+  lines = _LineRangesToSet(lines)
   _MarkLinesToFormat(uwlines, lines)
-  reformatted_source = reformatter.Reformat(uwlines, verify)
+  reformatted_source = reformatter.Reformat(uwlines, verify, lines)
 
   if unformatted_source == reformatted_source:
     return '' if print_diff else reformatted_source, False
@@ -210,40 +211,25 @@ DISABLE_PATTERN = r'^#.*\byapf:\s*disable\b'
 ENABLE_PATTERN = r'^#.*\byapf:\s*enable\b'
 
 
+def _LineRangesToSet(line_ranges):
+  """Return a set of lines in the range."""
+
+  if line_ranges is None:
+    return None
+
+  line_set = set()
+  for low, high in sorted(line_ranges):
+    line_set.update(range(low, high + 1))
+
+  return line_set
+
+
 def _MarkLinesToFormat(uwlines, lines):
   """Skip sections of code that we shouldn't reformat."""
   if lines:
     for uwline in uwlines:
-      uwline.disable = True
-
-    # Sort and combine overlapping ranges.
-    lines = sorted(lines)
-    line_ranges = [lines[0]] if len(lines[0]) else []
-    index = 1
-    while index < len(lines):
-      current = line_ranges[-1]
-      if lines[index][0] <= current[1]:
-        # The ranges overlap, so combine them.
-        line_ranges[-1] = (current[0], max(lines[index][1], current[1]))
-      else:
-        line_ranges.append(lines[index])
-      index += 1
-
-    # Mark lines to format as not being disabled.
-    index = 0
-    for start, end in sorted(line_ranges):
-      while index < len(uwlines) and uwlines[index].last.lineno < start:
-        index += 1
-      if index >= len(uwlines):
-        break
-
-      while index < len(uwlines):
-        if uwlines[index].lineno > end:
-          break
-        if (uwlines[index].lineno >= start or
-            uwlines[index].last.lineno >= start):
-          uwlines[index].disable = False
-        index += 1
+      uwline.disable = not (
+          lines.intersection(range(uwline.lineno, uwline.last.lineno + 1)))
 
   # Now go through the lines and disable any lines explicitly marked as
   # disabled.


Test patch:
diff --git a/yapftests/blank_line_calculator_test.py b/yapftests/blank_line_calculator_test.py
index 07710ea..3bdff94 100644
--- a/yapftests/blank_line_calculator_test.py
+++ b/yapftests/blank_line_calculator_test.py
@@ -18,6 +18,7 @@ import unittest
 
 from yapf.yapflib import reformatter
 from yapf.yapflib import style
+from yapf.yapflib import yapf_api
 
 from yapftests import yapf_test_helper
 
@@ -276,6 +277,79 @@ class BasicBlankLineCalculatorTest(yapf_test_helper.YAPFTest):
     uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
     self.assertCodeEqual(expected_formatted_code, reformatter.Reformat(uwlines))
 
+  def testLinesOnRangeBoundary(self):
+    unformatted_code = textwrap.dedent(u"""\
+        def A():
+          pass
+
+        def B():  # 4
+          pass  # 5
+
+        def C():
+          pass
+        def D():  # 9
+          pass  # 10
+        def E():
+          pass
+        """)
+    expected_formatted_code = textwrap.dedent(u"""\
+        def A():
+          pass
+
+
+        def B():  # 4
+          pass  # 5
+
+
+        def C():
+          pass
+
+
+        def D():  # 9
+          pass  # 10
+
+
+        def E():
+          pass
+        """)
+    code, changed = yapf_api.FormatCode(
+        unformatted_code, lines=[(4, 5), (9, 10)])
+    self.assertCodeEqual(expected_formatted_code, code)
+    self.assertTrue(changed)
+
+  def testLinesRangeBoundaryNotOutside(self):
+    unformatted_code = textwrap.dedent(u"""\
+        def A():
+          pass
+
+
+
+        def B():  # 6
+          pass  # 7
+
+
+
+        def C():
+          pass
+        """)
+    expected_formatted_code = textwrap.dedent(u"""\
+        def A():
+          pass
+
+
+
+        def B():  # 6
+          pass  # 7
+
+
+
+        def C():
+          pass
+        """)
+    code, changed = yapf_api.FormatCode(unformatted_code, lines=[(6, 7)])
+    self.assertCodeEqual(expected_formatted_code, code)
+    self.assertFalse(changed)
+
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/yapftests/yapf_test.py b/yapftests/yapf_test.py
index c9caa03..0ec093a 100644
--- a/yapftests/yapf_test.py
+++ b/yapftests/yapf_test.py
@@ -365,7 +365,7 @@ class CommandLineTest(unittest.TestCase):
         stderr=subprocess.PIPE)
     reformatted_code, stderrdata = p.communicate(unformatted.encode('utf-8'))
     self.assertEqual(stderrdata, b'')
-    self.assertEqual(reformatted_code.decode('utf-8'), expected)
+    self.assertMultiLineEqual(reformatted_code.decode('utf-8'), expected)
 
   def testUnicodeEncodingPipedToFile(self):
     unformatted_code = textwrap.dedent(u"""\
@@ -775,6 +775,8 @@ class CommandLineTest(unittest.TestCase):
         """)
     expected_formatted_code = textwrap.dedent("""\
         a = line_to_format
+
+
         def f():
             x = y + 42; z = n * 42
             if True: a += 1 ; b += 1 ; c += 1
@@ -797,6 +799,8 @@ class CommandLineTest(unittest.TestCase):
         ''')
     expected_formatted_code = textwrap.dedent('''\
         foo = 42
+
+
         def f():
             email_text += """<html>This is a really long docstring that goes over the column limit and is multi-line.<br><br>
         <b>Czar: </b>"""+despot["Nicholas"]+"""<br>
@@ -1041,6 +1045,7 @@ class CommandLineTest(unittest.TestCase):
         """)
     expected_formatted_code = textwrap.dedent("""\
         def foo():
+
           def bar():
             return {msg_id: author for author, msg_id in reader}
         """)