about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authortoonn <toonn@toonn.io>2020-07-05 15:45:39 +0200
committertoonn <toonn@toonn.io>2020-07-05 15:45:39 +0200
commit710156c371ac726257f1068546b0da889a26494c (patch)
tree8fe98f4c1c56be19f75c475d1e3b1fec5972ce76
parentaff694730d3df63778923e3f59cc77ff58c9235a (diff)
parentdfa60b5a8ed179cda8e262b98739913544278e46 (diff)
downloadranger-710156c371ac726257f1068546b0da889a26494c.tar.gz
Merge branch 'pylint3k'
-rw-r--r--.github/workflows/doctest.yml6
-rw-r--r--.github/workflows/py37.yml30
-rw-r--r--.github/workflows/python.yml4
-rw-r--r--.pylintrc7
-rw-r--r--.travis.yml17
-rw-r--r--ranger/config/.pylintrc2
-rwxr-xr-xranger/config/commands.py6
-rw-r--r--ranger/container/directory.py3
-rw-r--r--ranger/container/fsobject.py6
-rw-r--r--ranger/container/tags.py10
-rw-r--r--ranger/core/actions.py8
-rw-r--r--ranger/core/fm.py12
-rw-r--r--ranger/core/main.py1
-rw-r--r--ranger/ext/img_display.py9
-rw-r--r--ranger/ext/macrodict.py1
-rw-r--r--ranger/ext/safe_path.py1
-rw-r--r--ranger/ext/shell_escape.py2
-rw-r--r--ranger/ext/vcs/vcs.py10
-rw-r--r--ranger/gui/ansi.py24
-rw-r--r--ranger/gui/displayable.py7
-rw-r--r--ranger/gui/ui.py4
-rw-r--r--ranger/gui/widgets/browsercolumn.py2
-rw-r--r--requirements.txt2
-rw-r--r--tests/pylint/py2_compat.py126
-rw-r--r--tests/pylint/test_py2_compat.py157
-rw-r--r--tests/ranger/core/test_main.py1
26 files changed, 356 insertions, 102 deletions
diff --git a/.github/workflows/doctest.yml b/.github/workflows/doctest.yml
index f12cb926..4f3f8e7b 100644
--- a/.github/workflows/doctest.yml
+++ b/.github/workflows/doctest.yml
@@ -1,4 +1,4 @@
-name: Python doctest and pytest
+name: Python pytest, doctest and manpage-completion EXPECTED FAILURE
 
 on:
   push:
@@ -12,7 +12,7 @@ jobs:
     strategy:
       max-parallel: 4
       matrix:
-        python-version: [2.7, 3.5, 3.6]
+        python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
     steps:
     - uses: actions/checkout@v1
       with:
@@ -27,4 +27,4 @@ jobs:
         pip install -r requirements.txt
     - name: doctest
       run: |
-        make test_doctest test_other
+        make test_pytest test_doctest test_other
diff --git a/.github/workflows/py37.yml b/.github/workflows/py37.yml
deleted file mode 100644
index ca8210a2..00000000
--- a/.github/workflows/py37.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: Python 3.7 lints and tests
-
-on:
-  push:
-    paths:
-      - '.github/workflows/py37.yml'
-      - '*.py'
-
-jobs:
-  test_py:
-    runs-on: ubuntu-latest
-    strategy:
-      max-parallel: 4
-      matrix:
-        python-version: [3.7]
-    steps:
-    - uses: actions/checkout@v1
-      with:
-        fetch-depth: 1
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v1
-      with:
-        python-version: ${{ matrix.python-version }}
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install -r <(sed 's/<2//' requirements.txt)
-    - name: Lint and test with pylint, flake8, doctest, pytest
-      run: |
-        make test_py
diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
index 1daba84c..59558a20 100644
--- a/.github/workflows/python.yml
+++ b/.github/workflows/python.yml
@@ -12,7 +12,7 @@ jobs:
     strategy:
       max-parallel: 4
       matrix:
-        python-version: [2.7, 3.5, 3.6]
+        python-version: [3.5, 3.6, 3.7, 3.8]
     steps:
     - uses: actions/checkout@v1
       with:
@@ -25,6 +25,6 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install -r requirements.txt
-    - name: Lint and test with pylint, flake8, -d-o-c-t-e-s-t-, -p-y-t-e-s-t-
+    - name: Lint and test with pylint, flake8, but not -d-o-c-t-e-s-t-, -m-a-n-c-o-m-p-l-e-t-e-
       run: |
         make test_pylint test_flake8 test_pytest
diff --git a/.pylintrc b/.pylintrc
index 75bb7baf..3fdcbaa8 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,3 +1,7 @@
+[MASTER]
+init-hook='import sys; sys.path.append("tests/pylint")'
+load-plugins=py2_compat
+
 [BASIC]
 good-names=i,j,k,n,x,y,ex,Run,_,fm,ui,fg,bg
 bad-names=foo,baz,toto,tutu,tata
@@ -8,7 +12,8 @@ max-branches=16
 
 [FORMAT]
 max-line-length = 99
-disable=locally-disabled,locally-enabled,missing-docstring,duplicate-code,fixme,cyclic-import,redefined-variable-type,stop-iteration-return
+enable=no-absolute-import,old-division
+disable=cyclic-import,duplicate-code,fixme,import-outside-toplevel,locally-disabled,locally-enabled,missing-docstring,no-else-break,no-else-continue,no-else-raise,no-else-return,redefined-variable-type,stop-iteration-return,useless-object-inheritance
 
 [TYPECHECK]
 ignored-classes=ranger.core.actions.Actions
diff --git a/.travis.yml b/.travis.yml
index 7a156b2b..ce55f33e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,20 @@
 dist: 'xenial'
 
 language: 'python'
-python:
-  - '2.7'
-  - '3.5'
-  - '3.6'
+jobs:
+  include:
+    - name: "Python 2.7 no linting"
+      python: '2.7'
+      env: PY2='TRUE'
+    - name: "Python 3"
+      python:
+        - '3.5'
+        - '3.6'
+        - '3.7'
+        - '3.8'
 
 install:
   - 'pip install -r requirements.txt'
 
 script:
-  - 'make test'
+  - 'if [ -z "${PY2}" ]; then make test; else make test_doctest test_pytest test_other; fi'
diff --git a/ranger/config/.pylintrc b/ranger/config/.pylintrc
index 7cac80d1..cc79f97c 100644
--- a/ranger/config/.pylintrc
+++ b/ranger/config/.pylintrc
@@ -5,4 +5,4 @@ class-rgx=[a-z][a-z0-9_]{1,30}$
 [FORMAT]
 max-line-length = 99
 max-module-lines=3000
-disable=locally-disabled,locally-enabled,missing-docstring,duplicate-code,fixme
+disable=duplicate-code,fixme,import-outside-toplevel,locally-disabled,locally-enabled,missing-docstring,no-else-return
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index 27919541..b7b02e73 100755
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -338,7 +338,7 @@ class open_with(Command):
     def execute(self):
         app, flags, mode = self._get_app_flags_mode(self.rest(1))
         self.fm.execute_file(
-            files=[f for f in self.fm.thistab.get_selection()],
+            files=self.fm.thistab.get_selection(),
             app=app,
             flags=flags,
             mode=mode)
@@ -698,7 +698,7 @@ class delete(Command):
         return self._tab_directory_content()
 
     def _question_callback(self, files, answer):
-        if answer == 'y' or answer == 'Y':
+        if answer.lower() == 'y':
             self.fm.delete(files)
 
 
@@ -756,7 +756,7 @@ class trash(Command):
         return self._tab_directory_content()
 
     def _question_callback(self, files, answer):
-        if answer == 'y' or answer == 'Y':
+        if answer.lower() == 'y':
             self.fm.execute_file(files, label='trash')
 
 
diff --git a/ranger/container/directory.py b/ranger/container/directory.py
index 81dabb24..cabd4d65 100644
--- a/ranger/container/directory.py
+++ b/ranger/container/directory.py
@@ -74,7 +74,7 @@ def accept_file(fobj, filters):
 
 def walklevel(some_dir, level):
     some_dir = some_dir.rstrip(os.path.sep)
-    followlinks = True if level > 0 else False
+    followlinks = level > 0
     assert os.path.isdir(some_dir)
     num_sep = some_dir.count(os.path.sep)
     for root, dirs, files in os.walk(some_dir, followlinks=followlinks):
@@ -508,6 +508,7 @@ class Directory(  # pylint: disable=too-many-instance-attributes,too-many-public
 
     def sort(self):
         """Sort the contained files"""
+        # pylint: disable=comparison-with-callable
         if self.files_all is None:
             return
 
diff --git a/ranger/container/fsobject.py b/ranger/container/fsobject.py
index 7de889bf..51eb5376 100644
--- a/ranger/container/fsobject.py
+++ b/ranger/container/fsobject.py
@@ -296,7 +296,7 @@ class FileSystemObject(  # pylint: disable=too-many-instance-attributes,too-many
             if self.is_link:
                 new_stat = self.preload[0]
             self.preload = None
-            self.exists = True if new_stat else False
+            self.exists = bool(new_stat)
         else:
             try:
                 new_stat = lstat(path)
@@ -309,11 +309,11 @@ class FileSystemObject(  # pylint: disable=too-many-instance-attributes,too-many
 
         # Set some attributes
 
-        self.accessible = True if new_stat else False
+        self.accessible = bool(new_stat)
         mode = new_stat.st_mode if new_stat else 0
 
         fmt = mode & 0o170000
-        if fmt == 0o020000 or fmt == 0o060000:  # stat.S_IFCHR/BLK
+        if fmt in (0o020000, 0o060000):  # stat.S_IFCHR/BLK
             self.is_device = True
             self.size = 0
             self.infostring = 'dev'
diff --git a/ranger/container/tags.py b/ranger/container/tags.py
index ed5b876a..50d5ff72 100644
--- a/ranger/container/tags.py
+++ b/ranger/container/tags.py
@@ -29,10 +29,7 @@ class Tags(object):
         return item in self.tags
 
     def add(self, *items, **others):
-        if 'tag' in others:
-            tag = others['tag']
-        else:
-            tag = self.default_tag
+        tag = others.get('tag', self.default_tag)
         self.sync()
         for item in items:
             self.tags[item] = tag
@@ -48,10 +45,7 @@ class Tags(object):
         self.dump()
 
     def toggle(self, *items, **others):
-        if 'tag' in others:
-            tag = others['tag']
-        else:
-            tag = self.default_tag
+        tag = others.get('tag', self.default_tag)
         tag = str(tag)
         if tag not in ALLOWED_KEYS:
             return
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index 42a1d2f5..c42312ce 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -931,8 +931,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         except IndexError:
             self.ui.browser.draw_info = []
             return
-        programs = [program for program in self.rifle.list_commands([target.path], None,
-                                                                    skip_ask=True)]
+        programs = list(self.rifle.list_commands(
+            [target.path], None, skip_ask=True))
         if programs:
             num_digits = max((len(str(program[0])) for program in programs))
             program_info = ['%s | %s' % (str(program[0]).rjust(num_digits), program[1])
@@ -1026,7 +1026,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         sha.update(stat_.st_mtime)
         return '{0}.jpg'.format(sha.hexdigest())
 
-    def get_preview(self, fobj, width, height):  # pylint: disable=too-many-return-statements
+    def get_preview(self, fobj, width, height):
+        # pylint: disable=too-many-return-statements,too-many-statements
         pager = self.ui.get_pager()
         path = fobj.realpath
 
@@ -1324,7 +1325,6 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
             self.thistab = oldtab
             self.ui.titlebar.request_redraw()
             self.signal_emit('tab.layoutchange')
-        return None
 
     def tab_switch(self, path, create_directory=False):
         """Switches to tab of given path, opening a new tab as necessary.
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index 9461e325..da8b27c3 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -77,7 +77,7 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
         mimetypes.knownfiles.append(self.relpath('data/mime.types'))
         self.mimetypes = mimetypes.MimeTypes()
 
-    def initialize(self):
+    def initialize(self):  # pylint: disable=too-many-statements
         """If ui/bookmarks are None, they will be initialized here."""
 
         self.tabs = dict((n + 1, Tab(path)) for n, path in enumerate(self.start_paths))
@@ -271,15 +271,15 @@ class FM(Actions,  # pylint: disable=too-many-instance-attributes
                     shutil.copy(self.relpath(src), self.confpath(dest))
                 except OSError as ex:
                     sys.stderr.write("  ERROR: %s\n" % str(ex))
-        if which == 'rifle' or which == 'all':
+        if which in ('rifle', 'all'):
             copy('config/rifle.conf', 'rifle.conf')
-        if which == 'commands' or which == 'all':
+        if which in ('commands', 'all'):
             copy('config/commands_sample.py', 'commands.py')
-        if which == 'commands_full' or which == 'all':
+        if which in ('commands_full', 'all'):
             copy('config/commands.py', 'commands_full.py')
-        if which == 'rc' or which == 'all':
+        if which in ('rc', 'all'):
             copy('config/rc.conf', 'rc.conf')
-        if which == 'scope' or which == 'all':
+        if which in ('scope', 'all'):
             copy('data/scope.sh', 'scope.sh')
             os.chmod(self.confpath('scope.sh'),
                      os.stat(self.confpath('scope.sh')).st_mode | stat.S_IXUSR)
diff --git a/ranger/core/main.py b/ranger/core/main.py
index 8345060f..15943fec 100644
--- a/ranger/core/main.py
+++ b/ranger/core/main.py
@@ -396,6 +396,7 @@ def load_settings(  # pylint: disable=too-many-locals,too-many-branches,too-many
                 spec.loader.exec_module(module)
             elif (3, 3) <= sys.version_info < (3, 5):
                 from importlib.machinery import SourceFileLoader
+                # pylint: disable=no-value-for-parameter
                 module = SourceFileLoader(name, path).load_module()
             else:
                 import imp
diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py
index 20198ee1..f056db3b 100644
--- a/ranger/ext/img_display.py
+++ b/ranger/ext/img_display.py
@@ -108,15 +108,12 @@ class ImageDisplayer(object):
 
     def draw(self, path, start_x, start_y, width, height):
         """Draw an image at the given coordinates."""
-        pass
 
     def clear(self, start_x, start_y, width, height):
         """Clear a part of terminal display."""
-        pass
 
     def quit(self):
         """Cleanup and close"""
-        pass
 
 
 @register_image_displayer("w3m")
@@ -175,10 +172,8 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
     def draw(self, path, start_x, start_y, width, height):
         if not self.is_initialized or self.process.poll() is not None:
             self.initialize()
-        try:
-            input_gen = self._generate_w3m_input(path, start_x, start_y, width, height)
-        except ImageDisplayError:
-            raise
+        input_gen = self._generate_w3m_input(path, start_x, start_y, width,
+                                             height)
 
         # Mitigate the issue with the horizontal black bars when
         # selecting some images on some systems. 2 milliseconds seems
diff --git a/ranger/ext/macrodict.py b/ranger/ext/macrodict.py
index 8a8a4a3d..b4613fbc 100644
--- a/ranger/ext/macrodict.py
+++ b/ranger/ext/macrodict.py
@@ -1,3 +1,4 @@
+from __future__ import absolute_import
 import sys
 
 
diff --git a/ranger/ext/safe_path.py b/ranger/ext/safe_path.py
index b172b577..b2e360ea 100644
--- a/ranger/ext/safe_path.py
+++ b/ranger/ext/safe_path.py
@@ -1,6 +1,7 @@
 # This file is part of ranger, the console file manager.
 # License: GNU GPL version 3, see the file "AUTHORS" for details.
 
+from __future__ import absolute_import
 import os
 
 SUFFIX = '_'
diff --git a/ranger/ext/shell_escape.py b/ranger/ext/shell_escape.py
index a652fab1..05bff1df 100644
--- a/ranger/ext/shell_escape.py
+++ b/ranger/ext/shell_escape.py
@@ -9,6 +9,8 @@ from __future__ import (absolute_import, division, print_function)
 META_CHARS = (' ', "'", '"', '`', '&', '|', ';', '#',
               '$', '!', '(', ')', '[', ']', '<', '>', '\t')
 UNESCAPABLE = set(map(chr, list(range(9)) + list(range(10, 32)) + list(range(127, 256))))
+# pylint: disable=consider-using-dict-comprehension
+# COMPAT Dictionary comprehensions didn't exist before 2.7
 META_DICT = dict([(mc, '\\' + mc) for mc in META_CHARS])
 
 
diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py
index e2838f8d..bd744458 100644
--- a/ranger/ext/vcs/vcs.py
+++ b/ranger/ext/vcs/vcs.py
@@ -21,7 +21,6 @@ except ImportError:
 
 class VcsError(Exception):
     """VCS exception"""
-    pass
 
 
 class Vcs(object):  # pylint: disable=too-many-instance-attributes
@@ -77,8 +76,9 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
         )
 
         self.root, self.repodir, self.repotype, self.links = self._find_root(self.path)
-        self.is_root = True if self.obj.path == self.root else False
-        self.is_root_link = True if self.obj.is_link and self.obj.realpath == self.root else False
+        self.is_root = self.obj.path == self.root
+        self.is_root_link = (
+            self.obj.is_link and self.obj.realpath == self.root)
         self.is_root_pointer = self.is_root or self.is_root_link
         self.in_repodir = False
         self.rootvcs = None
@@ -513,19 +513,15 @@ from .svn import SVN  # NOQA pylint: disable=wrong-import-position
 
 class BzrRoot(VcsRoot, Bzr):
     """Bzr root"""
-    pass
 
 
 class GitRoot(VcsRoot, Git):
     """Git root"""
-    pass
 
 
 class HgRoot(VcsRoot, Hg):
     """Hg root"""
-    pass
 
 
 class SVNRoot(VcsRoot, SVN):
     """SVN root"""
-    pass
diff --git a/ranger/gui/ansi.py b/ranger/gui/ansi.py
index ce735317..3d33688d 100644
--- a/ranger/gui/ansi.py
+++ b/ranger/gui/ansi.py
@@ -42,23 +42,23 @@ def text_with_fg_bg_attr(ansi_text):  # pylint: disable=too-many-branches,too-ma
             for x256fg, x256bg, arg in codesplit_re.findall(attr_args + ';'):
                 # first handle xterm256 codes
                 try:
-                    if x256fg:                    # xterm256 foreground
+                    if x256fg:       # xterm256 foreground
                         fg = int(x256fg)
                         continue
-                    elif x256bg:                  # xterm256 background
+                    elif x256bg:     # xterm256 background
                         bg = int(x256bg)
                         continue
-                    elif arg:                     # usual ansi code
+                    elif arg:        # usual ansi code
                         n = int(arg)
-                    else:                         # empty code means reset
+                    else:            # empty code means reset
                         n = 0
                 except ValueError:
                     continue
 
-                if n == 0:                        # reset colors and attributes
+                if n == 0:           # reset colors and attributes
                     fg, bg, attr = -1, -1, 0
 
-                elif n == 1:                      # enable attribute
+                elif n == 1:         # enable attribute
                     attr |= color.bold
                 elif n == 4:
                     attr |= color.underline
@@ -69,7 +69,7 @@ def text_with_fg_bg_attr(ansi_text):  # pylint: disable=too-many-branches,too-ma
                 elif n == 8:
                     attr |= color.invisible
 
-                elif n == 22:                     # disable attribute
+                elif n == 22:        # disable attribute
                     attr &= not color.bold
                 elif n == 24:
                     attr &= not color.underline
@@ -80,21 +80,21 @@ def text_with_fg_bg_attr(ansi_text):  # pylint: disable=too-many-branches,too-ma
                 elif n == 28:
                     attr &= not color.invisible
 
-                elif n >= 30 and n <= 37:         # 8 ansi foreground and background colors
+                elif 30 <= n <= 37:  # 8 ansi foreground and background colors
                     fg = n - 30
                 elif n == 39:
                     fg = -1
-                elif n >= 40 and n <= 47:
+                elif 40 <= n <= 47:
                     bg = n - 40
                 elif n == 49:
                     bg = -1
 
                 # 8 aixterm high intensity colors (light but not bold)
-                elif n >= 90 and n <= 97:
+                elif 90 <= n <= 97:
                     fg = n - 90 + 8
                 elif n == 99:
                     fg = -1
-                elif n >= 100 and n <= 107:
+                elif 100 <= n <= 107:
                     bg = n - 100 + 8
                 elif n == 109:
                     bg = -1
@@ -159,7 +159,7 @@ def char_slice(ansi_text, start, length):
         pos += len(chunk)
         if pos <= start:
             pass  # seek
-        elif old_pos < start and pos >= start:
+        elif old_pos < start <= pos:
             chunks.append(last_color)
             chunks.append(str(chunk[start - old_pos:start - old_pos + length]))
         elif pos > length + start:
diff --git a/ranger/gui/displayable.py b/ranger/gui/displayable.py
index 1c3fb3e4..0a8e09c6 100644
--- a/ranger/gui/displayable.py
+++ b/ranger/gui/displayable.py
@@ -110,22 +110,20 @@ class Displayable(  # pylint: disable=too-many-instance-attributes
 
         x and y should be absolute coordinates.
         """
-        return (x >= self.x and x < self.x + self.wid) and \
-            (y >= self.y and y < self.y + self.hei)
+        return (self.x <= x < self.x + self.wid) and \
+            (self.y <= y < self.y + self.hei)
 
     def click(self, event):
         """Called when a mouse key is pressed and self.focused is True.
 
         Override this!
         """
-        pass
 
     def press(self, key):
         """Called when a key is pressed and self.focused is True.
 
         Override this!
         """
-        pass
 
     def poke(self):
         """Called before drawing, even if invisible"""
@@ -141,7 +139,6 @@ class Displayable(  # pylint: disable=too-many-instance-attributes
 
         Override this!
         """
-        pass
 
     def resize(self, y, x, hei=None, wid=None):
         """Resize the widget"""
diff --git a/ranger/gui/ui.py b/ranger/gui/ui.py
index a2ea7778..f0f81c88 100644
--- a/ranger/gui/ui.py
+++ b/ranger/gui/ui.py
@@ -239,7 +239,7 @@ class UI(  # pylint: disable=too-many-instance-attributes,too-many-public-method
         key = self.win.getch()
         if key == curses.KEY_ENTER:
             key = ord('\n')
-        if key == 27 or (key >= 128 and key < 256):
+        if key == 27 or (128 <= key < 256):
             # Handle special keys like ALT+X or unicode here:
             keys = [key]
             previous_load_mode = self.load_mode
@@ -534,7 +534,7 @@ class UI(  # pylint: disable=too-many-instance-attributes,too-many-public-method
                 self.fm.notify("Could not restore multiplexer window name!",
                                bad=True)
 
-            sys.stdout.write("\033k{}\033\\".format(self._multiplexer_title))
+            sys.stdout.write("\033k{0}\033\\".format(self._multiplexer_title))
             sys.stdout.flush()
 
     def hint(self, text=None):
diff --git a/ranger/gui/widgets/browsercolumn.py b/ranger/gui/widgets/browsercolumn.py
index 8632cc46..330823b3 100644
--- a/ranger/gui/widgets/browsercolumn.py
+++ b/ranger/gui/widgets/browsercolumn.py
@@ -539,7 +539,7 @@ class BrowserColumn(Pager):  # pylint: disable=too-many-instance-attributes
             self.target.scroll_begin = dirsize - winsize
             return self._get_scroll_begin()
 
-        if projected < upper_limit and projected > lower_limit:
+        if lower_limit < projected < upper_limit:
             return original
 
         if projected > upper_limit:
diff --git a/requirements.txt b/requirements.txt
index 411a2a97..fc51e82a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
 flake8
-pylint<2
+pylint
 pytest
diff --git a/tests/pylint/py2_compat.py b/tests/pylint/py2_compat.py
new file mode 100644
index 00000000..f3c4398c
--- /dev/null
+++ b/tests/pylint/py2_compat.py
@@ -0,0 +1,126 @@
+from __future__ import absolute_import
+
+import astroid
+
+from pylint.checkers import BaseChecker
+from pylint.interfaces import IAstroidChecker, HIGH
+
+from pylint.checkers import utils
+
+
+class Py2CompatibilityChecker(BaseChecker):
+    """Verify some simple properties of code compatible with both 2 and 3"""
+
+    __implements__ = IAstroidChecker
+
+    # The name defines a custom section of the config for this checker.
+    name = "py2-compat"
+    # The priority indicates the order that pylint will run the checkers.
+    priority = -1
+    # This class variable declares the messages (ie the warnings and errors)
+    # that the checker can emit.
+    msgs = {
+        # Each message has a code, a message that the user will see,
+        # a unique symbol that identifies the message,
+        # and a detailed help message
+        # that will be included in the documentation.
+        "E4200": ('Use explicit inheritance from "object"',
+                  "old-style-class",
+                  'Python 2 requires explicit inheritance from "object"'
+                  ' for new-style classes.'),
+        # old-division
+        # "E4210": ('Use "//" for integer division or import from "__future__"',
+        #     "division-without-import",
+        #     'Python 2 might perform integer division unless you import'
+        #     ' "division" from "__future__".'),
+        # no-absolute-import
+        # "E4211": ('Always import "absolute_import" from "__future__"',
+        #     "old-no-absolute-import",
+        #     'Python 2 allows relative imports unless you import'
+        #     ' "absolute_import" from "__future__".'),
+        "E4212": ('Import "print_function" from "__future__"',
+                  "print-without-import",
+                  'Python 2 requires importing "print_function" from'
+                  ' "__future__" to use the "print()" function syntax.'),
+        "E4220": ('Use explicit format spec numbering',
+                  "implicit-format-spec",
+                  'Python 2.6 does not support implicit format spec numbering'
+                  ' "{}", use explicit numbering "{0}" or keywords "{key}".')
+    }
+    # This class variable declares the options
+    # that are configurable by the user.
+    options = ()
+
+    def visit_classdef(self, node):
+        """Make sure classes explicitly inherit from object"""
+        if not node.bases and node.type == 'class' and not node.metaclass():
+            # We use confidence HIGH here because this message should only ever
+            # be emitted for classes at the root of the inheritance hierarchy
+            self.add_message('old-style-class', node=node, confidence=HIGH)
+
+    def visit_call(self, node):
+        """Make sure "print_function" is imported if necessary"""
+        if (isinstance(node.func, astroid.nodes.Name)
+                and node.func.name == "print"):
+            if "print_function" in node.root().future_imports:
+                def previous(node):
+                    if node is not None:
+                        parent = node.parent
+                    previous = node.previous_sibling()
+                    if previous is None:
+                        return parent
+                    return previous
+
+                prev = previous(node)
+                while prev is not None:
+                    if (isinstance(prev, astroid.nodes.ImportFrom)
+                        and prev.modname == "__future__"
+                        and "print_function" in (name_alias[0] for name_alias in
+                                                 prev.names)):
+                        return
+                    prev = previous(prev)
+
+                self.add_message("print-without-import", node=node,
+                                 confidence=HIGH)
+            else:
+                self.add_message("print-without-import", node=node,
+                                 confidence=HIGH)
+
+        func = utils.safe_infer(node.func)
+        if (
+            isinstance(func, astroid.BoundMethod)
+            and isinstance(func.bound, astroid.Instance)
+            and func.bound.name in ("str", "unicode", "bytes")
+        ):
+            if func.name == "format":
+                if isinstance(node.func, astroid.Attribute) and not isinstance(
+                    node.func.expr, astroid.Const
+                ):
+                    return
+                if node.starargs or node.kwargs:
+                    return
+                try:
+                    strnode = next(func.bound.infer())
+                except astroid.InferenceError:
+                    return
+                if not (isinstance(strnode, astroid.Const) and isinstance(
+                        strnode.value, str)):
+                    return
+                try:
+                    fields, num_args, manual_pos = (
+                        utils.parse_format_method_string(strnode.value)
+                    )
+                except utils.IncompleteFormatString:
+                    self.add_message("bad-format-string", node=node)
+                if num_args != 0:
+                    self.add_message("implicit-format-spec", node=node,
+                                     confidence=HIGH)
+
+
+def register(linter):
+    """This required method auto registers the checker.
+
+    :param linter: The linter to register the checker to.
+    :type linter: pylint.lint.PyLinter
+    """
+    linter.register_checker(Py2CompatibilityChecker(linter))
diff --git a/tests/pylint/test_py2_compat.py b/tests/pylint/test_py2_compat.py
new file mode 100644
index 00000000..a5d2e284
--- /dev/null
+++ b/tests/pylint/test_py2_compat.py
@@ -0,0 +1,157 @@
+from __future__ import absolute_import
+
+import py2_compat
+
+import astroid
+import pylint.testutils
+
+
+class TestPy2CompatibilityChecker(pylint.testutils.CheckerTestCase):
+    CHECKER_CLASS = py2_compat.Py2CompatibilityChecker
+
+    def test_oldstyle_class(self):
+        oldstyle_class, from_old = astroid.extract_node("""
+        class OldStyle(): #@
+            pass
+
+        class FromOld(OldStyle): #@
+            pass
+        """)
+
+        with self.assertAddsMessages(
+            pylint.testutils.Message(
+                msg_id='old-style-class',
+                node=oldstyle_class,
+            ),
+        ):
+            self.checker.visit_classdef(oldstyle_class)
+
+        with self.assertNoMessages():
+            self.checker.visit_classdef(from_old)
+
+    def test_newstyle_class(self):
+        newstyle_class, from_new = astroid.extract_node("""
+        class NewStyle(object): #@
+            pass
+        class FromNew(NewStyle): #@
+            pass
+        """)
+
+        with self.assertNoMessages():
+            self.checker.visit_classdef(newstyle_class)
+            self.checker.visit_classdef(from_new)
+
+    def test_print_without_import(self):
+        print_function_call = astroid.extract_node("""
+        print("Print function call without importing print_function")
+        """)
+
+        with self.assertAddsMessages(
+            pylint.testutils.Message(
+                msg_id='print-without-import',
+                node=print_function_call,
+            ),
+        ):
+            self.checker.visit_call(print_function_call)
+
+    def test_print_with_import(self):
+        print_function_call = astroid.extract_node("""
+        from __future__ import print_function
+        print("Print function call with importing print_function") #@
+        """)
+
+        nested_print_function_call = astroid.extract_node("""
+        def f():
+            from __future__ import print_function
+            class A():
+                def m(self):
+                    print("Nested print with import in scope") #@
+        """)
+
+        with self.assertNoMessages():
+            self.checker.visit_call(print_function_call)
+            self.checker.visit_call(nested_print_function_call)
+
+    def test_print_late_import(self):
+        early_print_function_call = astroid.extract_node("""
+        print("Nested print with import in scope") #@
+        def f():
+            from __future__ import print_function
+            class A():
+                def m(self):
+                    pass
+        """)
+
+        with self.assertAddsMessages(
+            pylint.testutils.Message(
+                msg_id='print-without-import',
+                node=early_print_function_call,
+            ),
+        ):
+            self.checker.visit_call(early_print_function_call)
+
+    def test_implicit_format_spec(self):
+        implicit_format_spec = astroid.extract_node("""
+        "{}".format("implicit") #@
+        """)
+
+        with self.assertAddsMessages(
+            pylint.testutils.Message(
+                msg_id='implicit-format-spec',
+                node=implicit_format_spec,
+            ),
+        ):
+            self.checker.visit_call(implicit_format_spec)
+
+    # # These checks still exist as old-division and no-absolute-import
+    # def test_division_without_import(self):
+    #     division = astroid.extract_node("""
+    #     5/2
+    #     """)
+
+    #     with self.assertAddsMessages(
+    #         pylint.testutils.Message(
+    #             msg_id='division-without-import',
+    #             node=division,
+    #         ),
+    #     ):
+    #         self.checker.visit_XXX(division)
+
+    # def test_division_with_import(self):
+    #     division = astroid.extract_node("""
+    #     from __future__ import division
+    #     5/2 #@
+    #     """)
+
+    #     with self.assertNoMessages():
+    #         self.checker.visit_XXX(division)
+
+    # def test_absolute_import(self):
+    #     no_import = astroid.extract_node("""
+    #     import sys
+    #     """)
+
+    #     with self.assertAddsMessages(
+    #         pylint.testutils.Message(
+    #             msg_id='old-no-absolute-import',
+    #             node=no_import,
+    #         ),
+    #     ):
+    #         self.checker.visit_XXX(no_import)
+
+    #     only_import = astroid.extract_node("""
+    #     from __future__ import absolute_import
+    #     """)
+
+    #     first_import = astroid.extract_node("""
+    #     from __future__ import absolute_import, print_function
+    #     """)
+
+    #     second_import = astroid.extract_node("""
+    #     from __future__ import print_function, absolute_import
+    #     """)
+
+    #     with self.assertNoMessages():
+    #         self.checker.visit_XXX(only_import)
+    #         self.checker.visit_XXX(first_import)
+    #         self.checker.visit_XXX(second_import)
diff --git a/tests/ranger/core/test_main.py b/tests/ranger/core/test_main.py
index d992b8a7..fd209bf9 100644
--- a/tests/ranger/core/test_main.py
+++ b/tests/ranger/core/test_main.py
@@ -1,3 +1,4 @@
+from __future__ import absolute_import
 import collections
 import os