Repository: google/yapf

Issue:
Space between modeline/shebang and module docstring
Imagine this file:

``` python
#!/usr/bin/env python
# -*- coding: utf-8 name> -*-

"""Some module docstring."""


def foobar():
    pass
```

Running yapf on it produces this diff:

``` diff
--- yapf.py (original)
+++ yapf.py (reformatted)
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 name> -*-
-
 """Some module docstring."""

```

I would actually have expected yapf **not** to remove the blank line between the shebang/encoding and the module docstring (i.e. do nothing).


Discussion:

Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 92806ad..e3cc518 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,6 +2,11 @@
 # All notable changes to this project will be documented in this file.
 # This project adheres to [Semantic Versioning](http://semver.org/).
 
+## Unreleased
+### Added
+- The `BLANK_LINE_BEFORE_MODULE_DOCSTRING` knob adds a blank line before a
+  module's docstring.
+
 ## [0.21.0] 2018-03-18
 ### Added
 - Introduce a new option of formatting multiline literals. Add
diff --git a/README.rst b/README.rst
index 3b43d29..ccc74d9 100644
--- a/README.rst
+++ b/README.rst
@@ -322,6 +322,9 @@ Knobs
             def method():
                 pass
 
+``BLANK_LINE_BEFORE_MODULE_DOCSTRING``
+    Insert a blank line before a module docstring.
+
 ``BLANK_LINE_BEFORE_CLASS_DOCSTRING``
     Insert a blank line before a class-level docstring.
 
diff --git a/yapf/yapflib/reformatter.py b/yapf/yapflib/reformatter.py
index a4965ab..0485d7e 100644
--- a/yapf/yapflib/reformatter.py
+++ b/yapf/yapflib/reformatter.py
@@ -473,6 +473,10 @@ def _CalculateNumberOfNewlines(first_token, indent_depth, prev_uwline,
         style.Get('BLANK_LINE_BEFORE_CLASS_DOCSTRING')):
       # Enforce a blank line before a class's docstring.
       return ONE_BLANK_LINE
+    elif (prev_uwline.first.value.startswith('#') and
+          style.Get('BLANK_LINE_BEFORE_MODULE_DOCSTRING')):
+      # Enforce a blank line before a module's docstring.
+      return ONE_BLANK_LINE
     # The docstring shouldn't have a newline before it.
     return NO_BLANK_LINES
 
diff --git a/yapf/yapflib/style.py b/yapf/yapflib/style.py
index 5f2fdac..7fd2177 100644
--- a/yapf/yapflib/style.py
+++ b/yapf/yapflib/style.py
@@ -71,6 +71,8 @@ _STYLE_HELP = dict(
             ..."""),
     BLANK_LINE_BEFORE_CLASS_DOCSTRING=textwrap.dedent("""\
       Insert a blank line before a class-level docstring."""),
+    BLANK_LINE_BEFORE_MODULE_DOCSTRING=textwrap.dedent("""\
+      Insert a blank line before a module docstring."""),
     BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION=textwrap.dedent("""\
       Number of blank lines surrounding top-level function and class
       definitions."""),
@@ -261,6 +263,7 @@ def CreatePEP8Style():
       ALLOW_SPLIT_BEFORE_DICT_VALUE=True,
       BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF=False,
       BLANK_LINE_BEFORE_CLASS_DOCSTRING=False,
+      BLANK_LINE_BEFORE_MODULE_DOCSTRING=False,
       BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION=2,
       COALESCE_BRACKETS=False,
       COLUMN_LIMIT=79,
@@ -406,6 +409,7 @@ _STYLE_OPTION_VALUE_CONVERTER = dict(
     ALLOW_SPLIT_BEFORE_DICT_VALUE=_BoolConverter,
     BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF=_BoolConverter,
     BLANK_LINE_BEFORE_CLASS_DOCSTRING=_BoolConverter,
+    BLANK_LINE_BEFORE_MODULE_DOCSTRING=_BoolConverter,
     BLANK_LINES_AROUND_TOP_LEVEL_DEFINITION=int,
     COALESCE_BRACKETS=_BoolConverter,
     COLUMN_LIMIT=int,


Test patch:
diff --git a/yapftests/reformatter_basic_test.py b/yapftests/reformatter_basic_test.py
index a1ca779..01ed3d9 100644
--- a/yapftests/reformatter_basic_test.py
+++ b/yapftests/reformatter_basic_test.py
@@ -2218,6 +2218,59 @@ s = 'foo \\
     finally:
       style.SetGlobalStyle(style.CreateChromiumStyle())
 
+  def testBlankLineBeforeModuleDocstring(self):
+    unformatted_code = textwrap.dedent('''\
+        #!/usr/bin/env python
+        # -*- coding: utf-8 name> -*-
+
+        """Some module docstring."""
+
+
+        def foobar():
+          pass
+        ''')
+    expected_code = textwrap.dedent('''\
+        #!/usr/bin/env python
+        # -*- coding: utf-8 name> -*-
+        """Some module docstring."""
+
+
+        def foobar():
+          pass
+        ''')
+    uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
+    self.assertEqual(expected_code, reformatter.Reformat(uwlines))
+
+    try:
+      style.SetGlobalStyle(
+          style.CreateStyleFromConfig(
+              '{based_on_style: pep8, '
+              'blank_line_before_module_docstring: True}'))
+      unformatted_code = textwrap.dedent('''\
+        #!/usr/bin/env python
+        # -*- coding: utf-8 name> -*-
+        """Some module docstring."""
+
+
+        def foobar():
+            pass
+          ''')
+      expected_formatted_code = textwrap.dedent('''\
+        #!/usr/bin/env python
+        # -*- coding: utf-8 name> -*-
+
+        """Some module docstring."""
+
+
+        def foobar():
+            pass
+          ''')
+      uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
+      self.assertCodeEqual(expected_formatted_code,
+                           reformatter.Reformat(uwlines))
+    finally:
+      style.SetGlobalStyle(style.CreateChromiumStyle())
+
   def testTupleCohesion(self):
     unformatted_code = textwrap.dedent("""\
         def f():