Repository: oppia/oppia

Issue:
Linting Checks Fail with No Error Message
<!--
  - Thanks for taking the time to report a bug in the Oppia project.
  - Before filing a new issue, please do a quick search to check that it hasn't
  - already been filed on the [issue tracker](https://github.com/oppia/oppia/issues)._
  -->

**Describe the bug**
Linter CI Check fails and outputs error logs with no error message. Not sure if other PRs are also suffering this problem.

**To Reproduce**
Steps to reproduce the behavior:
 1. On PR https://github.com/oppia/oppia/pull/9828, check the lint check logs.

**Observed behavior**
Linter CI Check fails and outputs error logs with no error message. 

**Expected behavior**
It shouldn't error out with no errors.

**Screenshots**
<img width="946" alt="Screen Shot 2020-07-10 at 9 35 53 AM" src="https://user-images.githubusercontent.com/7833572/87189407-72d25200-c2a5-11ea-99b0-147f248d51ed.png">


**Additional context**

Linting Checks Fail with No Error Message
<!--
  - Thanks for taking the time to report a bug in the Oppia project.
  - Before filing a new issue, please do a quick search to check that it hasn't
  - already been filed on the [issue tracker](https://github.com/oppia/oppia/issues)._
  -->

**Describe the bug**
Linter CI Check fails and outputs error logs with no error message. Not sure if other PRs are also suffering this problem.

**To Reproduce**
Steps to reproduce the behavior:
 1. On PR https://github.com/oppia/oppia/pull/9828, check the lint check logs.

**Observed behavior**
Linter CI Check fails and outputs error logs with no error message. 

**Expected behavior**
It shouldn't error out with no errors.

**Screenshots**
<img width="946" alt="Screen Shot 2020-07-10 at 9 35 53 AM" src="https://user-images.githubusercontent.com/7833572/87189407-72d25200-c2a5-11ea-99b0-147f248d51ed.png">


**Additional context**



Discussion:

Resolving patch:
diff --git a/scripts/linters/python_linter.py b/scripts/linters/python_linter.py
--- a/scripts/linters/python_linter.py
+++ b/scripts/linters/python_linter.py
@@ -105,45 +105,6 @@ def all_filepaths(self):
         """Return all filepaths."""
         return self.py_filepaths
 
-    def _check_import_order(self):
-        """This function is used to check that each file
-        has imports placed in alphabetical order.
-        """
-        if self.verbose_mode_enabled:
-            python_utils.PRINT('Starting import-order checks')
-            python_utils.PRINT('----------------------------------------')
-        summary_messages = []
-        files_to_check = self.py_filepaths
-        failed = False
-        stdout = sys.stdout
-        with linter_utils.redirect_stdout(stdout):
-            for filepath in files_to_check:
-                # This line prints the error message along with file path
-                # and returns True if it finds an error else returns False
-                # If check is set to True, isort simply checks the file and
-                # if check is set to False, it autocorrects import-order errors.
-                if (isort.SortImports(
-                        filepath, check=True, show_diff=(
-                            True)).incorrectly_sorted):
-                    failed = True
-                    python_utils.PRINT('')
-
-            python_utils.PRINT('')
-            if failed:
-                summary_message = (
-                    '%s Import order checks failed, file imports should be '
-                    'alphabetized, see affect files above.' % (
-                        linter_utils.FAILED_MESSAGE_PREFIX))
-                python_utils.PRINT(summary_message)
-                summary_messages.append(summary_message)
-            else:
-                summary_message = (
-                    '%s Import order checks passed' % (
-                        linter_utils.SUCCESS_MESSAGE_PREFIX))
-                python_utils.PRINT(summary_message)
-                summary_messages.append(summary_message)
-        return summary_messages
-
     def _check_non_test_files(self):
         """This function is used to check that function
            with test_only in their names are in test files.
@@ -314,12 +275,11 @@ def perform_all_lint_checks(self):
             python_utils.PRINT('There are no Python files to lint.')
             return []
 
-        import_order_check_message = self._check_import_order()
         job_registry_check_message = (
             self._check_that_all_jobs_are_listed_in_the_job_registry_file())
         test_function_check_message = self._check_non_test_files()
-        all_messages = import_order_check_message + job_registry_check_message
-        all_messages = all_messages + test_function_check_message
+        all_messages = (
+            job_registry_check_message + test_function_check_message)
         return all_messages
 
 
@@ -394,7 +354,7 @@ def _lint_py_files(self, config_pylint, config_pycodestyle):
         """
         files_to_lint = self.all_filepaths
         start_time = time.time()
-        are_there_errors = False
+        errors_found = False
         summary_messages = []
 
         num_py_files = len(files_to_lint)
@@ -417,14 +377,23 @@ def _lint_py_files(self, config_pylint, config_pycodestyle):
                 python_utils.PRINT('Linting Python files %s to %s...' % (
                     current_batch_start_index + 1, current_batch_end_index))
 
+            # This line invokes Pylint and redirect its output
+            # to the StringMessageStream.
+            pylint_report = StringMessageStream()
+            pylinter = lint.Run(
+                current_files_to_lint + [config_pylint],
+                reporter=text.TextReporter(pylint_report),
+                exit=False).linter
+
+            if pylinter.msg_status != 0:
+                for message in pylint_report.read():
+                    python_utils.PRINT(message)
+                pylint_error_messages = (
+                    self._get_trimmed_error_output(pylint_report.read()))
+                summary_messages.append(pylint_error_messages)
+                errors_found = True
+
             with linter_utils.redirect_stdout(stdout):
-                # This line invokes Pylint and prints its output
-                # to the target stdout.
-                pylint_report = StringMessageStream()
-                pylinter = lint.Run(
-                    current_files_to_lint + [config_pylint],
-                    reporter=text.TextReporter(pylint_report),
-                    exit=False).linter
                 # These lines invoke Pycodestyle and print its output
                 # to the target stdout.
                 style_guide = pycodestyle.StyleGuide(
@@ -432,19 +401,15 @@ def _lint_py_files(self, config_pylint, config_pycodestyle):
                 pycodestyle_report = style_guide.check_files(
                     paths=current_files_to_lint)
 
-            if pylinter.msg_status != 0 or pycodestyle_report.get_count() != 0:
+            if pycodestyle_report.get_count() != 0:
                 summary_message = stdout.getvalue()
-                for message in pylint_report.read():
-                    python_utils.PRINT(message)
-                pylint_error_messages = (
-                    self._get_trimmed_error_output(pylint_report.read()))
-                summary_messages.append(pylint_error_messages)
                 python_utils.PRINT(summary_message)
-                are_there_errors = True
+                summary_messages.append(summary_message)
+                errors_found = True
 
             current_batch_start_index = current_batch_end_index
 
-        if are_there_errors:
+        if errors_found:
             summary_message = (
                 '%s Python linting failed' % (
                     linter_utils.FAILED_MESSAGE_PREFIX))
@@ -470,7 +435,6 @@ def _lint_py_files_for_python3_compatibility(self):
         files_to_lint = self.all_filepaths
         start_time = time.time()
         any_errors = False
-        stdout = python_utils.string_io()
         summary_messages = []
 
         files_to_lint_for_python3_compatibility = [
@@ -505,21 +469,19 @@ def _lint_py_files_for_python3_compatibility(self):
                     'Linting Python files for Python 3 compatibility %s to %s..'
                     % (current_batch_start_index + 1, current_batch_end_index))
 
-            with linter_utils.redirect_stdout(stdout):
-                # This line invokes Pylint and prints its output
-                # to the target stdout.
-                python_utils.PRINT('Messages for Python 3 support:')
-                pylint_report = StringMessageStream()
-                pylinter_for_python3 = lint.Run(
-                    current_files_to_lint + ['--py3k'],
-                    reporter=text.TextReporter(pylint_report),
-                    exit=False).linter
+            # This line invokes Pylint and redirect its output
+            # to the StringMessageStream.
+            pylint_report = StringMessageStream()
+            pylinter_for_python3 = lint.Run(
+                current_files_to_lint + ['--py3k'],
+                reporter=text.TextReporter(pylint_report),
+                exit=False).linter
 
             if pylinter_for_python3.msg_status != 0:
-                summary_message = stdout.getvalue()
                 pylint_error_messages = (
                     self._get_trimmed_error_output(pylint_report.read()))
                 summary_messages.append(pylint_error_messages)
+                python_utils.PRINT('Messages for Python 3 support:')
                 for message in pylint_report.read():
                     python_utils.PRINT(message)
                 any_errors = True
@@ -544,6 +506,47 @@ def _lint_py_files_for_python3_compatibility(self):
             'Python linting for Python 3 compatibility finished.')
         return summary_messages
 
+    def _check_import_order(self):
+        """This function is used to check that each file
+        has imports placed in alphabetical order.
+        """
+        if self.verbose_mode_enabled:
+            python_utils.PRINT('Starting import-order checks')
+            python_utils.PRINT('----------------------------------------')
+        summary_messages = []
+        files_to_check = self.all_filepaths
+        failed = False
+        stdout = python_utils.string_io()
+        with linter_utils.redirect_stdout(stdout):
+            for filepath in files_to_check:
+                # This line prints the error message along with file path
+                # and returns True if it finds an error else returns False
+                # If check is set to True, isort simply checks the file and
+                # if check is set to False, it autocorrects import-order errors.
+                if (isort.SortImports(
+                        filepath, check=True, show_diff=(
+                            True)).incorrectly_sorted):
+                    failed = True
+                    python_utils.PRINT('')
+
+            python_utils.PRINT('')
+            if failed:
+                summary_message = stdout.getvalue()
+                summary_messages.append(summary_message)
+                summary_message = (
+                    '%s Import order checks failed, file imports should be '
+                    'alphabetized, see affect files above.' % (
+                        linter_utils.FAILED_MESSAGE_PREFIX))
+                python_utils.PRINT(summary_message)
+                summary_messages.append(summary_message)
+            else:
+                summary_message = (
+                    '%s Import order checks passed' % (
+                        linter_utils.SUCCESS_MESSAGE_PREFIX))
+                python_utils.PRINT(summary_message)
+                summary_messages.append(summary_message)
+        return summary_messages
+
     def perform_all_lint_checks(self):
         """Perform all the lint checks and returns the messages returned by all
         the checks.
@@ -568,6 +571,8 @@ def perform_all_lint_checks(self):
 
         all_messages.extend(self._lint_py_files_for_python3_compatibility())
 
+        all_messages.extend(self._check_import_order())
+
         return all_messages
 
 


Test patch:
diff --git a/scripts/linters/python_linter_test.py b/scripts/linters/python_linter_test.py
--- a/scripts/linters/python_linter_test.py
+++ b/scripts/linters/python_linter_test.py
@@ -36,6 +36,8 @@
     LINTER_TESTS_DIR, 'invalid_import_order.py')
 INVALID_TEST_ONLY_FILEPATH = os.path.join(
     LINTER_TESTS_DIR, 'invalid_test_only.py')
+INVALID_PYCODESTYLE_FILEPATH = os.path.join(
+    LINTER_TESTS_DIR, 'invalid_pycodestyle_error.py')
 
 INVALID_JOBS_ONE_OFF_FILEPATHS = [
     'scripts/linters/test_files/invalid_duplicate_jobs_one_off.py']
@@ -73,17 +75,16 @@ def mock_print(*args):
 
     def test_unsorted_import_order(self):
         with self.print_swap:
-            python_linter.PythonLintChecksManager(
-                [INVALID_IMPORT_FILEPATH], FILE_CACHE,
-                True).perform_all_lint_checks()
+            python_linter.ThirdPartyPythonLintChecksManager(
+                [INVALID_IMPORT_FILEPATH], True).perform_all_lint_checks()
         self.assert_same_list_elements([
             'FAILED  Import order checks failed, file imports should be '
             'alphabetized, see affect files above.'], self.linter_stdout)
 
     def test_sorted_import_order(self):
         with self.print_swap:
-            python_linter.PythonLintChecksManager(
-                [VALID_PY_FILEPATH], FILE_CACHE, True).perform_all_lint_checks()
+            python_linter.ThirdPartyPythonLintChecksManager(
+                [VALID_PY_FILEPATH], True).perform_all_lint_checks()
         self.assert_same_list_elements(
             ['SUCCESS  Import order checks passed'],
             self.linter_stdout)
@@ -189,6 +190,13 @@ def test_third_party_linter_with_no_files(self):
             ['There are no Python files to lint.'],
             self.linter_stdout)
 
+    def test_pycodestyle_with_error_message(self):
+        summary_messages = python_linter.ThirdPartyPythonLintChecksManager(
+            [INVALID_PYCODESTYLE_FILEPATH], True).perform_all_lint_checks()
+        self.assert_same_list_elements(
+            ['27:1: E302 expected 2 blank lines, found 1'],
+            summary_messages)
+
     def test_get_linters_with_success(self):
         custom_linter, third_party_linter = python_linter.get_linters(
             [VALID_PY_FILEPATH], FILE_CACHE, verbose_mode_enabled=True)
diff --git a/scripts/linters/test_files/invalid_pycodestyle_error.py b/scripts/linters/test_files/invalid_pycodestyle_error.py
new file mode 100644
--- /dev/null
+++ b/scripts/linters/test_files/invalid_pycodestyle_error.py
@@ -0,0 +1,42 @@
+# coding: utf-8
+#
+# Copyright 2020 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Python file with invalid syntax, used by scripts/linters/
+python_linter_test.py. This file contains just one newline between imports and
+class defintion but two newlines are required on line 26.
+"""
+
+from __future__ import absolute_import  # pylint: disable=import-only-modules
+from __future__ import unicode_literals  # pylint: disable=import-only-modules
+
+import python_utils
+
+class FakeClass(python_utils.OBJECT):
+    """This is a fake docstring for valid syntax purposes."""
+
+    def __init__(self, fake_arg):
+        self.fake_arg = fake_arg
+
+    def fake_method(self, name):
+        """This doesn't do anything.
+
+        Args:
+            name: str. Means nothing.
+
+        Yields:
+            tuple(str, str). The argument passed in but twice in a tuple.
+        """
+        yield (name, name)