Repository: spyder-ide/spyder

Issue:
Move PyLint plugin to Python 3
Take into account the following:

- [ ] Update `super()` use
- [ ] Remove the `py3compat` module
- [ ] Update any code (and remove if necessary) that uses `if PY2` etc...
- [ ] Check division operator
- [ ] Remove future imports
- [ ] Check tests
- [ ] Check for Py2 of Py3 codes


Discussion:

Resolving patch:
diff --git a/spyder/plugins/pylint/plugin.py b/spyder/plugins/pylint/plugin.py
--- a/spyder/plugins/pylint/plugin.py
+++ b/spyder/plugins/pylint/plugin.py
@@ -46,7 +46,7 @@ class Pylint(SpyderPluginWidget):
     CONF_FILE = False
 
     def __init__(self, parent=None):
-        SpyderPluginWidget.__init__(self, parent)
+        super().__init__(parent)
 
         max_entries = self.get_option('max_entries', 50)
         self.pylint = PylintWidget(self, max_entries=max_entries,
@@ -72,7 +72,7 @@ def __init__(self, parent=None):
         # analysis
         self.pylint.start_analysis.connect(self.run_pylint_from_analyze_button)
 
-    #------ SpyderPluginWidget API --------------------------------------------
+    # ------ SpyderPluginWidget API -------------------------------------------
     def get_plugin_title(self):
         """Return widget title"""
         return _("Code Analysis")
@@ -124,14 +124,13 @@ def apply_plugin_settings(self, options):
         # next Spyder startup, which is soon enough
         pass
 
-    #------ Public API --------------------------------------------------------
+    # ----- Public API --------------------------------------------------------
     @Slot()
     def change_history_depth(self):
         "Change history max entries"""
-        depth, valid = QInputDialog.getInt(self, _('History'),
-                                       _('Maximum entries'),
-                                       self.get_option('max_entries'),
-                                       10, 10000)
+        depth, valid = QInputDialog.getInt(
+            self, _('History'), _('Maximum entries'),
+            self.get_option('max_entries'), 10, 10000)
         if valid:
             self.set_option('max_entries', depth)
 
diff --git a/spyder/plugins/pylint/widgets/pylintgui.py b/spyder/plugins/pylint/widgets/pylintgui.py
--- a/spyder/plugins/pylint/widgets/pylintgui.py
+++ b/spyder/plugins/pylint/widgets/pylintgui.py
@@ -12,8 +12,8 @@
 # pylint: disable=R0201
 
 # Standard library imports
-from __future__ import print_function, with_statement
 import os.path as osp
+import pickle
 import re
 import sys
 import time
@@ -27,7 +27,6 @@
 
 # Local imports
 from spyder.config.base import get_conf_path, get_translation
-from spyder.py3compat import pickle, to_text_string
 from spyder.utils import icon_manager as ima
 from spyder.utils.qthelpers import create_toolbutton
 from spyder.utils.misc import getcwd_or_home
@@ -41,18 +40,20 @@
 # This is needed for testing this module as a stand alone script
 try:
     _ = get_translation("pylint", "spyder_pylint")
-except KeyError as error:
+except KeyError:
     import gettext
     _ = gettext.gettext
 
 
 PYLINT_VER = pylint.__version__
-#TODO: display results on 3 columns instead of 1: msg_id, lineno, message
+
+
+# TODO: display results on 3 columns instead of 1: msg_id, lineno, message
 class ResultsTree(OneColumnTree):
     sig_edit_goto = Signal(str, int, str)
 
     def __init__(self, parent):
-        OneColumnTree.__init__(self, parent)
+        super().__init__(parent)
         self.filename = None
         self.results = None
         self.data = None
@@ -94,7 +95,7 @@ def refresh(self):
                    ima.icon('error'), self.results['E:']))
         for title, icon, messages in results:
             title += ' (%d message%s)' % (len(messages),
-                                          's' if len(messages)>1 else '')
+                                          's' if len(messages) > 1 else '')
             title_item = QTreeWidgetItem(self, [title], QTreeWidgetItem.Type)
             title_item.setIcon(0, icon)
             if not messages:
@@ -131,7 +132,8 @@ def refresh(self):
                     text = "[%s] %d : %s" % (msg_id, lineno, message)
                 else:
                     text = "%d : %s" % (lineno, message)
-                msg_item = QTreeWidgetItem(parent, [text], QTreeWidgetItem.Type)
+                msg_item = QTreeWidgetItem(parent, [text],
+                                           QTreeWidgetItem.Type)
                 msg_item.setIcon(0, ima.icon('arrow'))
                 self.data[id(msg_item)] = (modname, lineno)
 
@@ -147,7 +149,7 @@ class PylintWidget(QWidget):
 
     def __init__(self, parent, max_entries=100, options_button=None,
                  text_color=None, prevrate_color=None):
-        QWidget.__init__(self, parent)
+        super().__init__(parent)
 
         self.setWindowTitle("Pylint")
 
@@ -169,30 +171,37 @@ def __init__(self, parent, max_entries=100, options_button=None,
 
         self.filecombo = PythonModulesComboBox(self)
 
-        self.start_button = create_toolbutton(self, icon=ima.icon('run'),
-                                    text=_("Analyze"),
-                                    tip=_("Run analysis"),
-                                    triggered=self.analyze_button_handler,
-                                    text_beside_icon=True)
-        self.stop_button = create_toolbutton(self,
-                                             icon=ima.icon('stop'),
-                                             text=_("Stop"),
-                                             tip=_("Stop current analysis"),
-                                             text_beside_icon=True)
+        self.start_button = create_toolbutton(
+            self,
+            icon=ima.icon('run'),
+            text=_("Analyze"),
+            tip=_("Run analysis"),
+            triggered=self.analyze_button_handler,
+            text_beside_icon=True)
+        self.stop_button = create_toolbutton(
+            self,
+            icon=ima.icon('stop'),
+            text=_("Stop"),
+            tip=_("Stop current analysis"),
+            text_beside_icon=True)
         self.filecombo.valid.connect(self.start_button.setEnabled)
         self.filecombo.valid.connect(self.check_new_file)
 
-        browse_button = create_toolbutton(self, icon=ima.icon('fileopen'),
-                               tip=_('Select Python file'),
-                               triggered=self.select_file)
+        browse_button = create_toolbutton(
+            self,
+            icon=ima.icon('fileopen'),
+            tip=_('Select Python file'),
+            triggered=self.select_file)
 
         self.ratelabel = QLabel()
         self.datelabel = QLabel()
-        self.log_button = create_toolbutton(self, icon=ima.icon('log'),
-                                    text=_("Output"),
-                                    text_beside_icon=True,
-                                    tip=_("Complete output"),
-                                    triggered=self.show_log)
+        self.log_button = create_toolbutton(
+            self,
+            icon=ima.icon('log'),
+            text=_("Output"),
+            text_beside_icon=True,
+            tip=_("Complete output"),
+            triggered=self.show_log)
         self.treewidget = ResultsTree(self)
 
         hlayout1 = QHBoxLayout()
@@ -240,7 +249,7 @@ def get_filename(self):
     @Slot(str)
     def set_filename(self, filename):
         """Set filename without performing code analysis."""
-        filename = to_text_string(filename) # filename is a QString instance
+        filename = str(filename)  # filename is a QString instance
         self.kill_if_running()
         index, _data = self.get_data(filename)
         if index is None:
@@ -330,7 +339,7 @@ def get_pylintrc_path(self, filename):
     @Slot()
     def start(self):
         """Start the code analysis."""
-        filename = to_text_string(self.filecombo.currentText())
+        filename = str(self.filecombo.currentText())
 
         self.process = QProcess(self)
         self.process.setProcessChannelMode(QProcess.SeparateChannels)
@@ -388,7 +397,7 @@ def read_output(self, error=False):
                 qba += self.process.readAllStandardError()
             else:
                 qba += self.process.readAllStandardOutput()
-        text = to_text_string(qba.data(), encoding='utf-8')
+        text = str(qba.data(), 'utf-8')
         if error:
             self.error_output += text
         else:
@@ -406,7 +415,7 @@ def finished(self, exit_code, exit_status):
         results = {'C:': [], 'R:': [], 'W:': [], 'E:': []}
         txt_module = '************* Module '
 
-        module = '' # Should not be needed - just in case something goes wrong
+        module = ''  # Should not be needed - just in case something goes wrong
         for line in self.output.splitlines():
             if line.startswith(txt_module):
                 # New module
@@ -448,8 +457,7 @@ def finished(self, exit_code, exit_status):
                 i_prun_end = self.output.find('/10', i_prun)
                 previous = self.output[i_prun+len(txt_prun):i_prun_end]
 
-
-        filename = to_text_string(self.filecombo.currentText())
+        filename = str(self.filecombo.currentText())
         self.set_data(filename, (time.localtime(), rate, previous, results))
         self.output = self.error_output + self.output
         self.show_data(justanalyzed=True)
@@ -463,10 +471,10 @@ def kill_if_running(self):
     def show_data(self, justanalyzed=False):
         if not justanalyzed:
             self.output = None
-        self.log_button.setEnabled(self.output is not None \
+        self.log_button.setEnabled(self.output is not None
                                    and len(self.output) > 0)
         self.kill_if_running()
-        filename = to_text_string(self.filecombo.currentText())
+        filename = str(self.filecombo.currentText())
         if not filename:
             return
 
@@ -499,9 +507,7 @@ def show_data(self, justanalyzed=False):
                     text_prun = ' (%s %s/10)' % (text_prun, previous_rate)
                     text += prevrate_style % (self.prevrate_color, text_prun)
                 self.treewidget.set_results(filename, results)
-                date = to_text_string(time.strftime("%Y-%m-%d %H:%M:%S",
-                                                    datetime),
-                                      encoding='utf8')
+                date = time.strftime("%Y-%m-%d %H:%M:%S", datetime)
                 date_text = text_style % (self.text_color, date)
 
         self.ratelabel.setText(text)


Test patch:
diff --git a/spyder/plugins/pylint/tests/test_pylint.py b/spyder/plugins/pylint/tests/test_pylint.py
--- a/spyder/plugins/pylint/tests/test_pylint.py
+++ b/spyder/plugins/pylint/tests/test_pylint.py
@@ -7,10 +7,6 @@
 
 """Tests for the execution of pylint."""
 
-
-# Future imports
-from __future__ import unicode_literals
-
 # Standard library imports
 from io import open
 import os.path as osp
@@ -21,8 +17,6 @@
 # Local imports
 from spyder.plugins.pylint.widgets.pylintgui import PylintWidget
 from spyder.plugins.pylint.utils import get_pylintrc_path
-from spyder.py3compat import PY2
-
 
 # pylint: disable=redefined-outer-name
 
@@ -134,7 +128,6 @@ def test_pylint_widget_noproject(pylint_test_script, qtbot):
     assert pylint_data[1] is not None
 
 
-@pytest.mark.skipif(PY2, reason="It fails on PY2 for no obvious reason")
 def test_pylint_widget_pylintrc(
         pylint_test_script, pylintrc_files, mocker, qtbot):
     """Test that entire pylint widget gets results depending on pylintrc."""
diff --git a/spyder/plugins/pylint/tests/test_pylint_config_dialog.py b/spyder/plugins/pylint/tests/test_pylint_config_dialog.py
--- a/spyder/plugins/pylint/tests/test_pylint_config_dialog.py
+++ b/spyder/plugins/pylint/tests/test_pylint_config_dialog.py
@@ -6,10 +6,7 @@
 # ----------------------------------------------------------------------------
 """Tests for plugin config dialog."""
 
-try:
-    from unittest.mock import Mock
-except ImportError:
-    from mock import Mock  # Python 2
+from unittest.mock import Mock
 
 # Third party imports
 from qtpy.QtCore import Signal, QObject
@@ -24,7 +21,7 @@ class MainWindowMock(QObject):
     sig_editor_focus_changed = Signal(str)
 
     def __init__(self):
-        super(MainWindowMock, self).__init__(None)
+        super().__init__(None)
         self.editor = Mock()
         self.editor.sig_editor_focus_changed = self.sig_editor_focus_changed