Repository: oppia/oppia

Issue:
Check for excessive empty lines between methods in python files
As per https://github.com/oppia/oppia/pull/7779#discussion_r337735624, there should not be too many empty lines between methods in python files. We should create a lint check for this issue. See https://www.python.org/dev/peps/pep-0008/#blank-lines
Check for excessive empty lines between methods in python files
As per https://github.com/oppia/oppia/pull/7779#discussion_r337735624, there should not be too many empty lines between methods in python files. We should create a lint check for this issue. See https://www.python.org/dev/peps/pep-0008/#blank-lines


Discussion:
@kevinlee12 can I work on this one?
sure!
@kevinlee12 can I work on this one?
sure!

Resolving patch:
diff --git a/core/controllers/base.py b/core/controllers/base.py
--- a/core/controllers/base.py
+++ b/core/controllers/base.py
@@ -51,7 +51,6 @@
     'Text used to encrypt CSRF tokens.', DEFAULT_CSRF_SECRET)
 
 
-
 def _clear_login_cookies(response_headers):
     """Clears login cookies from the given response headers."""
 
diff --git a/scripts/pylint_extensions.py b/scripts/pylint_extensions.py
--- a/scripts/pylint_extensions.py
+++ b/scripts/pylint_extensions.py
@@ -961,6 +961,65 @@ def process_module(self, node):
                 self.add_message('single-space-after-yield', line=line_num + 1)
 
 
+class ExcessiveEmptyLinesChecker(checkers.BaseChecker):
+    """Checks if there are excessive newlines between method definitions."""
+    __implements__ = interfaces.IRawChecker
+
+    name = 'excessive-new-lines'
+    priority = -1
+    msgs = {
+        'C0011': (
+            'Excessive new lines between function definations.',
+            'excessive-new-lines',
+            'Remove extra newlines.'
+        )
+    }
+
+    def process_module(self, node):
+        """Process a module to ensure that method definitions are not seperated
+        by more than two blank lines.
+
+        Args:
+            node: astroid.scoped_nodes.Function. Node to access module content.
+        """
+        in_multi_line_comment = False
+        multi_line_indicator = b'"""'
+        file_content = read_from_node(node)
+        file_length = len(file_content)
+        blank_line_counter = 0
+
+        for line_num in python_utils.RANGE(file_length):
+            line = file_content[line_num].strip()
+
+            # Single multi-line comment, ignore it.
+            if line.count(multi_line_indicator) == 2:
+                continue
+
+            # Flip multi-line boolean depending on whether or not we see
+            # the multi-line indicator. Possible for multiline comment to
+            # be somewhere other than the start of a line (e.g. func arg),
+            # so we can't look at start of or end of a line, which is why
+            # the case where two indicators in a single line is handled
+            # separately (i.e. one line comment with multi-line strings).
+            if multi_line_indicator in line:
+                in_multi_line_comment = not in_multi_line_comment
+
+            # Ignore anything inside a multi-line comment.
+            if in_multi_line_comment:
+                continue
+
+            if file_content[line_num] == b'\n':
+                blank_line_counter += 1
+
+            else:
+                blank_line_counter = 0
+
+            if line_num + 1 < file_length and blank_line_counter > 2:
+                line = file_content[line_num + 1].strip()
+                if line.startswith(b'def') or line.startswith(b'@'):
+                    self.add_message('excessive-new-lines', line=line_num + 1)
+
+
 def register(linter):
     """Registers the checker with pylint.
 
@@ -976,3 +1035,4 @@ def register(linter):
     linter.register_checker(RestrictedImportChecker(linter))
     linter.register_checker(SingleCharAndNewlineAtEOFChecker(linter))
     linter.register_checker(SingleSpaceAfterYieldChecker(linter))
+    linter.register_checker(ExcessiveEmptyLinesChecker(linter))


Test patch:
diff --git a/scripts/pylint_extensions_test.py b/scripts/pylint_extensions_test.py
--- a/scripts/pylint_extensions_test.py
+++ b/scripts/pylint_extensions_test.py
@@ -1046,3 +1046,100 @@ def test_no_space_after_yield_statement(self):
             ),
         ):
             temp_file.close()
+
+
+class ExcessiveEmptyLinesCheckerTests(unittest.TestCase):
+
+    def test_checks_excessive_empty_lines(self):
+        checker_test_object = testutils.CheckerTestCase()
+        checker_test_object.CHECKER_CLASS = (
+            pylint_extensions.ExcessiveEmptyLinesChecker)
+        checker_test_object.setup_method()
+        node_excessive_empty_lines = astroid.scoped_nodes.Module(
+            name='test',
+            doc='Custom test')
+        temp_file = tempfile.NamedTemporaryFile()
+        filename = temp_file.name
+
+        with python_utils.open_file(filename, 'w') as tmp:
+            tmp.write(
+                u"""def func1():
+                        returns_something
+
+
+
+                    def func2():
+                        returns_something
+                """)
+        node_excessive_empty_lines.file = filename
+        node_excessive_empty_lines.path = filename
+
+        checker_test_object.checker.process_module(node_excessive_empty_lines)
+
+        with checker_test_object.assertAddsMessages(
+            testutils.Message(
+                msg_id='excessive-new-lines',
+                line=5
+            ),
+        ):
+            temp_file.close()
+
+        node_method_with_decorator = astroid.scoped_nodes.Module(
+            name='test',
+            doc='Custom test')
+
+        temp_file = tempfile.NamedTemporaryFile()
+        filename = temp_file.name
+        with python_utils.open_file(filename, 'w') as tmp:
+            tmp.write(
+                u"""def func0():
+                        returns_something
+
+
+
+                    @something
+                    def func1():
+                        returns_something
+                """)
+        node_method_with_decorator.file = filename
+        node_method_with_decorator.path = filename
+
+        checker_test_object.checker.process_module(node_method_with_decorator)
+
+        with checker_test_object.assertAddsMessages(
+            testutils.Message(
+                msg_id='excessive-new-lines',
+                line=5
+            ),
+        ):
+            temp_file.close()
+
+        node_with_no_error_message = astroid.scoped_nodes.Module(
+            name='test',
+            doc='Custom test')
+
+        temp_file = tempfile.NamedTemporaryFile()
+        filename = temp_file.name
+        with python_utils.open_file(filename, 'w') as tmp:
+            tmp.write(
+                u"""def func0():
+                        returns_something
+
+                    def func1():
+                        returns_something
+
+
+                    def func2():
+                        returns_something
+
+                    @something
+                    def func3():
+                        returns_something
+                """)
+        node_with_no_error_message.file = filename
+        node_with_no_error_message.path = filename
+
+        checker_test_object.checker.process_module(node_with_no_error_message)
+
+        with checker_test_object.assertNoMessages():
+            temp_file.close()