about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/pylint.yml (renamed from .github/workflows/doctest.yml)20
-rw-r--r--.github/workflows/pypy.yml30
-rw-r--r--.github/workflows/python.yml18
-rw-r--r--.github/workflows/shellcheck.yml4
-rw-r--r--.travis.yml20
-rw-r--r--ranger/__init__.py16
-rwxr-xr-xranger/config/commands.py29
-rw-r--r--ranger/container/bookmarks.py27
-rw-r--r--ranger/container/history.py10
-rw-r--r--ranger/container/tags.py18
-rw-r--r--ranger/core/actions.py9
-rw-r--r--ranger/core/loader.py6
-rw-r--r--ranger/core/main.py20
-rw-r--r--ranger/core/runner.py3
-rw-r--r--ranger/ext/accumulator.py3
-rw-r--r--ranger/ext/img_display.py74
-rw-r--r--ranger/ext/macrodict.py2
-rw-r--r--ranger/ext/open23.py48
-rw-r--r--ranger/ext/popen_forked.py9
-rwxr-xr-xranger/ext/rifle.py62
-rw-r--r--ranger/ext/spawn.py8
-rw-r--r--ranger/ext/vcs/vcs.py2
-rw-r--r--ranger/gui/colorscheme.py4
-rw-r--r--ranger/gui/widgets/console.py45
-rwxr-xr-xtests/manpage_completion_test.py3
25 files changed, 288 insertions, 202 deletions
diff --git a/.github/workflows/doctest.yml b/.github/workflows/pylint.yml
index 4f3f8e7b..349c9af4 100644
--- a/.github/workflows/doctest.yml
+++ b/.github/workflows/pylint.yml
@@ -1,30 +1,28 @@
-name: Python pytest, doctest and manpage-completion EXPECTED FAILURE
+name: Pylint
 
 on:
   push:
     paths:
-      - '.github/workflows/doctest.yml'
-      - '*.py'
+      - '.github/workflows/pylint.yml'
+      - '**.py'
 
 jobs:
-  test_py:
+  test_pylint:
     runs-on: ubuntu-latest
     strategy:
       max-parallel: 4
       matrix:
-        python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
+        python-version: [3.6, 3.x]
     steps:
-    - uses: actions/checkout@v1
-      with:
-        fetch-depth: 1
+    - uses: actions/checkout@v2
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
         pip install -r requirements.txt
-    - name: doctest
+    - name: Lint with pylint
       run: |
-        make test_pytest test_doctest test_other
+        make test_pylint
diff --git a/.github/workflows/pypy.yml b/.github/workflows/pypy.yml
new file mode 100644
index 00000000..dfb04042
--- /dev/null
+++ b/.github/workflows/pypy.yml
@@ -0,0 +1,30 @@
+name: Pypy tests
+
+on:
+  push:
+    paths:
+      - '.github/workflows/pypy.yml'
+      - '**.py'
+
+jobs:
+  test_pypy:
+    runs-on: ubuntu-latest
+    strategy:
+      max-parallel: 4
+      matrix:
+        python-version: [pypy2, pypy3]
+    env:
+      TERM: dumb
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install -r requirements.txt
+    - name: Pypy lints and tests
+      run: |
+        make test_pylint test_flake8 test_pytest test_doctest test_other
diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
index 59558a20..82672bfb 100644
--- a/.github/workflows/python.yml
+++ b/.github/workflows/python.yml
@@ -1,10 +1,10 @@
-name: Python lints and tests
+name: PEP8 and tests
 
 on:
   push:
     paths:
       - '.github/workflows/python.yml'
-      - '*.py'
+      - '**.py'
 
 jobs:
   test_py:
@@ -12,19 +12,19 @@ jobs:
     strategy:
       max-parallel: 4
       matrix:
-        python-version: [3.5, 3.6, 3.7, 3.8]
+        python-version: [2.7, 3.5, 3.x]
+    env:
+      TERM: dumb
     steps:
-    - uses: actions/checkout@v1
-      with:
-        fetch-depth: 1
+    - uses: actions/checkout@v2
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v1
+      uses: actions/setup-python@v2
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
         pip install -r requirements.txt
-    - 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-
+    - name: Flake8 and test
       run: |
-        make test_pylint test_flake8 test_pytest
+        make test_flake8 test_pytest test_doctest test_other
diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml
index 22af007b..cd718a2d 100644
--- a/.github/workflows/shellcheck.yml
+++ b/.github/workflows/shellcheck.yml
@@ -10,9 +10,7 @@ jobs:
   test_shellcheck:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v1
-      with:
-        fetch-depth: 1
+    - uses: actions/checkout@v2
     - name: Install latest stable shellcheck
       run: |
         curl -LO "https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz"
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ce55f33e..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-dist: 'xenial'
-
-language: 'python'
-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:
-  - 'if [ -z "${PY2}" ]; then make test; else make test_doctest test_pytest test_other; fi'
diff --git a/ranger/__init__.py b/ranger/__init__.py
index b2d99115..cc388d60 100644
--- a/ranger/__init__.py
+++ b/ranger/__init__.py
@@ -22,14 +22,16 @@ def version_helper():
         import subprocess
         version_string = 'ranger-master {0}'
         try:
-            git_describe = subprocess.Popen(['git', 'describe'],
-                                            universal_newlines=True,
-                                            cwd=RANGERDIR,
-                                            stdout=subprocess.PIPE,
-                                            stderr=subprocess.PIPE)
-            (git_description, _) = git_describe.communicate()
+            with subprocess.Popen(
+                ["git", "describe"],
+                universal_newlines=True,
+                cwd=RANGERDIR,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+            ) as git_describe:
+                (git_description, _) = git_describe.communicate()
             version_string = version_string.format(git_description.strip('\n'))
-        except (OSError, subprocess.CalledProcessError):
+        except (OSError, subprocess.CalledProcessError, AttributeError):
             version_string = version_string.format(__version__)
     return version_string
 
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index 833430c6..0b61b6e4 100755
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -886,14 +886,14 @@ class load_copy_buffer(Command):
         fname = self.fm.datapath(self.copy_buffer_filename)
         unreadable = OSError if PY3 else IOError
         try:
-            fobj = open(fname, 'r')
+            with open(fname, "r") as fobj:
+                self.fm.copy_buffer = set(
+                    File(g) for g in fobj.read().split("\n") if exists(g)
+                )
         except unreadable:
             return self.fm.notify(
                 "Cannot open %s" % (fname or self.copy_buffer_filename), bad=True)
 
-        self.fm.copy_buffer = set(File(g)
-                                  for g in fobj.read().split("\n") if exists(g))
-        fobj.close()
         self.fm.ui.redraw_main_column()
         return None
 
@@ -910,12 +910,11 @@ class save_copy_buffer(Command):
         fname = self.fm.datapath(self.copy_buffer_filename)
         unwritable = OSError if PY3 else IOError
         try:
-            fobj = open(fname, 'w')
+            with open(fname, "w") as fobj:
+                fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer))
         except unwritable:
             return self.fm.notify("Cannot open %s" %
                                   (fname or self.copy_buffer_filename), bad=True)
-        fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer))
-        fobj.close()
         return None
 
 
@@ -963,7 +962,8 @@ class touch(Command):
         if not lexists(fname):
             if not lexists(dirname):
                 makedirs(dirname)
-            open(fname, 'a').close()
+            with open(fname, 'a'):
+                pass  # Just create the file
         else:
             self.fm.notify("file/directory exists!", bad=True)
 
@@ -1166,6 +1166,7 @@ class bulkrename(Command):
         import tempfile
         from ranger.container.file import File
         from ranger.ext.shell_escape import shell_escape as esc
+        from ranger.ext.open23 import open23
 
         # Create and edit the file list
         filenames = [f.relative_path for f in self.fm.thistab.get_selection()]
@@ -1177,8 +1178,9 @@ class bulkrename(Command):
             else:
                 listfile.write("\n".join(filenames))
         self.fm.execute_file([File(listpath)], app='editor')
-        with (open(listpath, 'r', encoding="utf-8", errors="surrogateescape") if
-              PY3 else open(listpath, 'r')) as listfile:
+        with open23(
+            listpath, "r", encoding="utf-8", errors="surrogateescape"
+        ) as listfile:
             new_filenames = listfile.read().split("\n")
         os.unlink(listpath)
         if all(a == b for a, b in zip(filenames, new_filenames)):
@@ -1980,9 +1982,10 @@ class yank(Command):
 
         new_clipboard_contents = "\n".join(selection)
         for command in clipboard_commands:
-            process = subprocess.Popen(command, universal_newlines=True,
-                                       stdin=subprocess.PIPE)
-            process.communicate(input=new_clipboard_contents)
+            with subprocess.Popen(
+                command, universal_newlines=True, stdin=subprocess.PIPE
+            ) as process:
+                process.communicate(input=new_clipboard_contents)
 
     def get_selection_attr(self, attr):
         return [getattr(item, attr) for item in
diff --git a/ranger/container/bookmarks.py b/ranger/container/bookmarks.py
index 7b5e56b7..d4264a61 100644
--- a/ranger/container/bookmarks.py
+++ b/ranger/container/bookmarks.py
@@ -175,15 +175,14 @@ class Bookmarks(FileManagerAware):
 
         path_new = self.path + '.new'
         try:
-            fobj = open(path_new, 'w')
+            with open(path_new, 'w') as fobj:
+                for key, value in self.dct.items():
+                    if isinstance(key, str) and key in ALLOWED_KEYS \
+                            and key not in self.nonpersistent_bookmarks:
+                        fobj.write("{0}:{1}\n".format(str(key), str(value)))
         except OSError as ex:
             self.fm.notify('Bookmarks error: {0}'.format(str(ex)), bad=True)
             return
-        for key, value in self.dct.items():
-            if isinstance(key, str) and key in ALLOWED_KEYS \
-                    and key not in self.nonpersistent_bookmarks:
-                fobj.write("{0}:{1}\n".format(str(key), str(value)))
-        fobj.close()
 
         try:
             old_perms = os.stat(self.path)
@@ -225,17 +224,17 @@ class Bookmarks(FileManagerAware):
                 return None
 
         try:
-            fobj = open(self.path, 'r')
+            with open(self.path, 'r') as fobj:
+                dct = {}
+                for line in fobj:
+                    if self.load_pattern.match(line):
+                        key, value = line[0], line[2:-1]
+                        if key in ALLOWED_KEYS:
+                            dct[key] = self.bookmarktype(value)
         except OSError as ex:
             self.fm.notify('Bookmarks error: {0}'.format(str(ex)), bad=True)
             return None
-        dct = {}
-        for line in fobj:
-            if self.load_pattern.match(line):
-                key, value = line[0], line[2:-1]
-                if key in ALLOWED_KEYS:
-                    dct[key] = self.bookmarktype(value)
-        fobj.close()
+
         return dct
 
     def _set_dict(self, dct, original):
diff --git a/ranger/container/history.py b/ranger/container/history.py
index 9361e373..93a5b1fe 100644
--- a/ranger/container/history.py
+++ b/ranger/container/history.py
@@ -108,17 +108,11 @@ class History(object):
             raise HistoryEmptyException
 
     def back(self):
-        self.index -= 1
-        if self.index < 0:
-            self.index = 0
+        self.index = max(0, self.index - 1)
         return self.current()
 
     def move(self, n):
-        self.index += n
-        if self.index > len(self.history) - 1:
-            self.index = len(self.history) - 1
-        if self.index < 0:
-            self.index = 0
+        self.index = max(0, min(len(self.history) - 1, self.index + n))
         return self.current()
 
     def search(self, string, n):
diff --git a/ranger/container/tags.py b/ranger/container/tags.py
index e61ed595..e2395897 100644
--- a/ranger/container/tags.py
+++ b/ranger/container/tags.py
@@ -8,8 +8,8 @@ from __future__ import (absolute_import, division, print_function)
 from os.path import exists, abspath, realpath, expanduser, sep
 import string
 
-from ranger import PY3
 from ranger.core.shared import FileManagerAware
+from ranger.ext.open23 import open23
 
 ALLOWED_KEYS = string.ascii_letters + string.digits + string.punctuation
 
@@ -75,27 +75,20 @@ class Tags(FileManagerAware):
 
     def sync(self):
         try:
-            if PY3:
-                fobj = open(self._filename, 'r', errors='replace')
-            else:
-                fobj = open(self._filename, 'r')
+            with open23(self._filename, "r", errors="replace") as fobj:
+                self.tags = self._parse(fobj)
         except (OSError, IOError) as err:
             if exists(self._filename):
                 self.fm.notify(err, bad=True)
             else:
                 self.tags = dict()
-        else:
-            self.tags = self._parse(fobj)
-            fobj.close()
 
     def dump(self):
         try:
-            fobj = open(self._filename, 'w')
+            with open(self._filename, 'w') as fobj:
+                self._compile(fobj)
         except OSError as err:
             self.fm.notify(err, bad=True)
-        else:
-            self._compile(fobj)
-            fobj.close()
 
     def _compile(self, fobj):
         for path, tag in self.tags.items():
@@ -128,6 +121,7 @@ class Tags(FileManagerAware):
             elif path.startswith(path_old + sep):
                 pnew = path_new + path[len(path_old):]
             if pnew:
+                # pylint: disable=unnecessary-dict-index-lookup
                 del self.tags[path]
                 self.tags[pnew] = tag
                 changed = True
diff --git a/ranger/core/actions.py b/ranger/core/actions.py
index 417647db..6b184236 100644
--- a/ranger/core/actions.py
+++ b/ranger/core/actions.py
@@ -1044,6 +1044,9 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         if not self.settings.preview_script or not self.settings.use_preview_script:
             try:
                 # XXX: properly determine file's encoding
+                # Disable the lint because the preview is read outside the
+                # local scope.
+                # pylint: disable=consider-using-with
                 return codecs.open(path, 'r', errors='ignore')
             # IOError for Python2, OSError for Python3
             except (IOError, OSError):
@@ -1407,6 +1410,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         if not contexts:
             contexts = 'browser', 'console', 'pager', 'taskview'
 
+        # Disable lint because TemporaryFiles are removed on close
+        # pylint: disable=consider-using-with
         temporary_file = tempfile.NamedTemporaryFile()
 
         def write(string):  # pylint: disable=redefined-outer-name
@@ -1432,6 +1437,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         self._run_pager(temporary_file.name)
 
     def dump_commands(self):
+        # Disable lint because TemporaryFiles are removed on close
+        # pylint: disable=consider-using-with
         temporary_file = tempfile.NamedTemporaryFile()
 
         def write(string):  # pylint: disable=redefined-outer-name
@@ -1457,6 +1464,8 @@ class Actions(  # pylint: disable=too-many-instance-attributes,too-many-public-m
         self._run_pager(temporary_file.name)
 
     def dump_settings(self):
+        # Disable lint because TemporaryFiles are removed on close
+        # pylint: disable=consider-using-with
         temporary_file = tempfile.NamedTemporaryFile()
 
         def write(string):  # pylint: disable=redefined-outer-name
diff --git a/ranger/core/loader.py b/ranger/core/loader.py
index dcee568a..ec94b049 100644
--- a/ranger/core/loader.py
+++ b/ranger/core/loader.py
@@ -170,7 +170,11 @@ class CommandLoader(  # pylint: disable=too-many-instance-attributes
         self.kill_on_pause = kill_on_pause
         self.popenArgs = popenArgs  # pylint: disable=invalid-name
 
-    def generate(self):  # pylint: disable=too-many-branches,too-many-statements
+    def generate(self):
+        # pylint: disable=too-many-branches,too-many-statements
+        # TODO: Check whether we can afford to wait for processes and use a
+        #       with-statement for Popen.
+        # pylint: disable=consider-using-with
         popenargs = {} if self.popenArgs is None else self.popenArgs
         popenargs['stdout'] = popenargs['stderr'] = PIPE
         popenargs['stdin'] = PIPE if self.input else open(os.devnull, 'r')
diff --git a/ranger/core/main.py b/ranger/core/main.py
index 8bfb03e7..03c9f9b6 100644
--- a/ranger/core/main.py
+++ b/ranger/core/main.py
@@ -13,7 +13,8 @@ import shutil
 import sys
 import tempfile
 
-from ranger import PY3, VERSION
+from ranger import VERSION
+from ranger.ext.open23 import open23
 
 
 LOG = getLogger(__name__)
@@ -74,14 +75,12 @@ def main(
             return 1
         fm = FM()
         try:
-            if PY3:
-                fobj = open(fm.datapath('tagged'), 'r', errors='replace')
-            else:
-                fobj = open(fm.datapath('tagged'), 'r')
+            with open23(fm.datapath('tagged'), 'r', errors='replace') as fobj:
+                lines = fobj.readlines()
         except OSError as ex:
             print('Unable to open `tagged` data file: {0}'.format(ex), file=sys.stderr)
             return 1
-        for line in fobj.readlines():
+        for line in lines:
             if len(line) > 2 and line[1] == ':':
                 if line[0] in args.list_tagged_files:
                     sys.stdout.write(line[2:])
@@ -390,7 +389,7 @@ def load_settings(  # pylint: disable=too-many-locals,too-many-branches,too-many
         def import_file(name, path):  # From https://stackoverflow.com/a/67692
             # pragma pylint: disable=no-name-in-module,import-error,no-member, deprecated-method
             if sys.version_info >= (3, 5):
-                import importlib.util as util
+                from importlib import util
                 spec = util.spec_from_file_location(name, path)
                 module = util.module_from_spec(spec)
                 spec.loader.exec_module(module)
@@ -399,7 +398,7 @@ def load_settings(  # pylint: disable=too-many-locals,too-many-branches,too-many
                 # pylint: disable=no-value-for-parameter
                 module = SourceFileLoader(name, path).load_module()
             else:
-                import imp
+                import imp  # pylint: disable=deprecated-module
                 module = imp.load_source(name, path)
             # pragma pylint: enable=no-name-in-module,import-error,no-member
             return module
@@ -444,8 +443,9 @@ def load_settings(  # pylint: disable=too-many-locals,too-many-branches,too-many
 
             if not os.path.exists(fm.confpath('plugins', '__init__.py')):
                 LOG.debug("Creating missing '__init__.py' file in plugin folder")
-                fobj = open(fm.confpath('plugins', '__init__.py'), 'w')
-                fobj.close()
+                with open(fm.confpath('plugins', '__init__.py'), 'w'):
+                    # Create the file if it doesn't exist.
+                    pass
 
             ranger.fm = fm
             for plugin in sorted(plugins):
diff --git a/ranger/core/runner.py b/ranger/core/runner.py
index d465f070..0a3aa171 100644
--- a/ranger/core/runner.py
+++ b/ranger/core/runner.py
@@ -190,6 +190,8 @@ class Runner(object):  # pylint: disable=too-few-public-methods
             pipe_output = True
             context.wait = False
         if 's' in context.flags:
+            # Using a with-statement for these is inconvenient.
+            # pylint: disable=consider-using-with
             devnull_writable = open(os.devnull, 'w')
             devnull_readable = open(os.devnull, 'r')
             for key in ('stdout', 'stderr'):
@@ -240,6 +242,7 @@ class Runner(object):  # pylint: disable=too-few-public-methods
                 if 'f' in context.flags and 'r' not in context.flags:
                     # This can fail and return False if os.fork() is not
                     # supported, but we assume it is, since curses is used.
+                    # pylint: disable=consider-using-with
                     Popen_forked(**popen_kws)
                 else:
                     process = Popen(**popen_kws)
diff --git a/ranger/ext/accumulator.py b/ranger/ext/accumulator.py
index c2e33b59..a41db634 100644
--- a/ranger/ext/accumulator.py
+++ b/ranger/ext/accumulator.py
@@ -75,8 +75,7 @@ class Accumulator(object):
                 i = 0
             if i >= len(lst):
                 i = len(lst) - 1
-            if i < 0:
-                i = 0
+            i = max(0, i)
 
             self.pointer = i
             self.pointed_obj = lst[i]
diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py
index b7e6572f..f3a36432 100644
--- a/ranger/ext/img_display.py
+++ b/ranger/ext/img_display.py
@@ -141,6 +141,8 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
         """start w3mimgdisplay"""
         self.binary_path = None
         self.binary_path = self._find_w3mimgdisplay_executable()  # may crash
+        # We cannot close the process because that stops the preview.
+        # pylint: disable=consider-using-with
         self.process = Popen([self.binary_path] + W3MIMGDISPLAY_OPTIONS, cwd=self.working_dir,
                              stdin=PIPE, stdout=PIPE, universal_newlines=True)
         self.is_initialized = True
@@ -165,8 +167,12 @@ class W3MImageDisplayer(ImageDisplayer, FileManagerAware):
         fretint = fcntl.ioctl(fd_stdout, termios.TIOCGWINSZ, farg)
         rows, cols, xpixels, ypixels = struct.unpack("HHHH", fretint)
         if xpixels == 0 and ypixels == 0:
-            process = Popen([self.binary_path, "-test"], stdout=PIPE, universal_newlines=True)
-            output, _ = process.communicate()
+            with Popen(
+                [self.binary_path, "-test"],
+                stdout=PIPE,
+                universal_newlines=True,
+            ) as process:
+                output, _ = process.communicate()
             output = output.split()
             xpixels, ypixels = int(output[0]), int(output[1])
             # adjust for misplacement
@@ -346,41 +352,37 @@ class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware):
     @staticmethod
     def _get_image_dimensions(path):
         """Determine image size using imghdr"""
-        file_handle = open(path, 'rb')
-        file_header = file_handle.read(24)
-        image_type = imghdr.what(path)
-        if len(file_header) != 24:
-            file_handle.close()
-            return 0, 0
-        if image_type == 'png':
-            check = struct.unpack('>i', file_header[4:8])[0]
-            if check != 0x0d0a1a0a:
-                file_handle.close()
+        with open(path, 'rb') as file_handle:
+            file_header = file_handle.read(24)
+            image_type = imghdr.what(path)
+            if len(file_header) != 24:
                 return 0, 0
-            width, height = struct.unpack('>ii', file_header[16:24])
-        elif image_type == 'gif':
-            width, height = struct.unpack('<HH', file_header[6:10])
-        elif image_type == 'jpeg':
-            unreadable = OSError if PY3 else IOError
-            try:
-                file_handle.seek(0)
-                size = 2
-                ftype = 0
-                while not 0xc0 <= ftype <= 0xcf:
-                    file_handle.seek(size, 1)
-                    byte = file_handle.read(1)
-                    while ord(byte) == 0xff:
+            if image_type == 'png':
+                check = struct.unpack('>i', file_header[4:8])[0]
+                if check != 0x0d0a1a0a:
+                    return 0, 0
+                width, height = struct.unpack('>ii', file_header[16:24])
+            elif image_type == 'gif':
+                width, height = struct.unpack('<HH', file_header[6:10])
+            elif image_type == 'jpeg':
+                unreadable = OSError if PY3 else IOError
+                try:
+                    file_handle.seek(0)
+                    size = 2
+                    ftype = 0
+                    while not 0xc0 <= ftype <= 0xcf:
+                        file_handle.seek(size, 1)
                         byte = file_handle.read(1)
-                    ftype = ord(byte)
-                    size = struct.unpack('>H', file_handle.read(2))[0] - 2
-                file_handle.seek(1, 1)
-                height, width = struct.unpack('>HH', file_handle.read(4))
-            except unreadable:
-                height, width = 0, 0
-        else:
-            file_handle.close()
-            return 0, 0
-        file_handle.close()
+                        while ord(byte) == 0xff:
+                            byte = file_handle.read(1)
+                        ftype = ord(byte)
+                        size = struct.unpack('>H', file_handle.read(2))[0] - 2
+                    file_handle.seek(1, 1)
+                    height, width = struct.unpack('>HH', file_handle.read(4))
+                except unreadable:
+                    height, width = 0, 0
+            else:
+                return 0, 0
         return width, height
 
 
@@ -740,6 +742,8 @@ class UeberzugImageDisplayer(ImageDisplayer):
                 and not self.process.stdin.closed):
             return
 
+        # We cannot close the process because that stops the preview.
+        # pylint: disable=consider-using-with
         self.process = Popen(['ueberzug', 'layer', '--silent'], cwd=self.working_dir,
                              stdin=PIPE, universal_newlines=True)
         self.is_initialized = True
diff --git a/ranger/ext/macrodict.py b/ranger/ext/macrodict.py
index b4613fbc..924fe5a9 100644
--- a/ranger/ext/macrodict.py
+++ b/ranger/ext/macrodict.py
@@ -15,7 +15,7 @@ def macro_val(thunk, fallback=MACRO_FAIL):
 try:
     from collections.abc import MutableMapping  # pylint: disable=no-name-in-module
 except ImportError:
-    from collections import MutableMapping
+    from collections import MutableMapping  # pylint: disable=deprecated-class
 
 
 class MacroDict(MutableMapping):
diff --git a/ranger/ext/open23.py b/ranger/ext/open23.py
new file mode 100644
index 00000000..de60d516
--- /dev/null
+++ b/ranger/ext/open23.py
@@ -0,0 +1,48 @@
+# 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
+
+from contextlib import contextmanager
+
+from ranger import PY3
+
+
+# COMPAT: We use the pattern of opening a file differently depending on the
+#         python version in multiple places. Having calls to open in multiple
+#         branches makes it impossible to use a with-statement instead. This
+#         contextmanager hides away the lack of an errors keyword argument for
+#         python 2 and is now preferred. This can be safely dropped once we
+#         ditch python 2 support.
+# pylint: disable=too-many-arguments
+@contextmanager
+def open23(
+    file,
+    mode="r",
+    buffering=-1,
+    encoding=None,
+    errors=None,
+    newline=None,
+    closefd=True,
+    opener=None,
+):
+    if PY3:
+        fobj = open(
+            file=file,
+            mode=mode,
+            buffering=buffering,
+            encoding=encoding,
+            errors=errors,
+            newline=newline,
+            closefd=closefd,
+            opener=opener,
+        )
+    else:
+        if buffering is None:
+            fobj = open(name=file, mode=mode)
+        else:
+            fobj = open(name=file, mode=mode, buffering=buffering)
+    try:
+        yield fobj
+    finally:
+        fobj.close()
diff --git a/ranger/ext/popen_forked.py b/ranger/ext/popen_forked.py
index bff1818e..85c8448b 100644
--- a/ranger/ext/popen_forked.py
+++ b/ranger/ext/popen_forked.py
@@ -18,9 +18,12 @@ def Popen_forked(*args, **kwargs):  # pylint: disable=invalid-name
         return False
     if pid == 0:
         os.setsid()
-        kwargs['stdin'] = open(os.devnull, 'r')
-        kwargs['stdout'] = kwargs['stderr'] = open(os.devnull, 'w')
-        subprocess.Popen(*args, **kwargs)
+        with open(os.devnull, 'r') as null_r, open(os.devnull, 'w') as null_w:
+            kwargs['stdin'] = null_r
+            kwargs['stdout'] = kwargs['stderr'] = null_w
+            with subprocess.Popen(*args, **kwargs):
+                # Just to enable using with
+                pass
         os._exit(0)  # pylint: disable=protected-access
     else:
         os.wait()
diff --git a/ranger/ext/rifle.py b/ranger/ext/rifle.py
index af75ac41..8985e232 100755
--- a/ranger/ext/rifle.py
+++ b/ranger/ext/rifle.py
@@ -80,9 +80,13 @@ except ImportError:
             return False
         if pid == 0:
             os.setsid()
-            kwargs['stdin'] = open(os.devnull, 'r')
-            kwargs['stdout'] = kwargs['stderr'] = open(os.devnull, 'w')
-            Popen(*args, **kwargs)
+            with open(os.devnull, "r") as null_r, open(
+                os.devnull, "w"
+            ) as null_w:
+                kwargs["stdin"] = null_r
+                kwargs["stdout"] = kwargs["stderr"] = null_w
+                with Popen(*args, **kwargs):
+                    pass
             os._exit(0)  # pylint: disable=protected-access
         return True
 
@@ -159,20 +163,19 @@ class Rifle(object):  # pylint: disable=too-many-instance-attributes
         """Replace the current configuration with the one in config_file"""
         if config_file is None:
             config_file = self.config_file
-        fobj = open(config_file, 'r')
-        self.rules = []
-        for line in fobj:
-            line = line.strip()
-            if line.startswith('#') or line == '':
-                continue
-            if self.delimiter1 not in line:
-                raise ValueError("Line without delimiter")
-            tests, command = line.split(self.delimiter1, 1)
-            tests = tests.split(self.delimiter2)
-            tests = tuple(tuple(f.strip().split(None, 1)) for f in tests)
-            command = command.strip()
-            self.rules.append((command, tests))
-        fobj.close()
+        with open(config_file, "r") as fobj:
+            self.rules = []
+            for line in fobj:
+                line = line.strip()
+                if line.startswith('#') or line == '':
+                    continue
+                if self.delimiter1 not in line:
+                    raise ValueError("Line without delimiter")
+                tests, command = line.split(self.delimiter1, 1)
+                tests = tests.split(self.delimiter2)
+                tests = tuple(tuple(f.strip().split(None, 1)) for f in tests)
+                command = command.strip()
+                self.rules.append((command, tests))
 
     def _eval_condition(self, condition, files, label):
         # Handle the negation of conditions starting with an exclamation mark,
@@ -256,14 +259,19 @@ class Rifle(object):  # pylint: disable=too-many-instance-attributes
         self._mimetype, _ = mimetypes.guess_type(fname)
 
         if not self._mimetype:
-            process = Popen(["file", "--mime-type", "-Lb", fname], stdout=PIPE, stderr=PIPE)
-            mimetype, _ = process.communicate()
+            with Popen(
+                ["file", "--mime-type", "-Lb", fname], stdout=PIPE, stderr=PIPE
+            ) as process:
+                mimetype, _ = process.communicate()
             self._mimetype = mimetype.decode(ENCODING).strip()
             if self._mimetype == 'application/octet-stream':
                 try:
-                    process = Popen(["mimetype", "--output-format", "%m", fname],
-                                    stdout=PIPE, stderr=PIPE)
-                    mimetype, _ = process.communicate()
+                    with Popen(
+                        ["mimetype", "--output-format", "%m", fname],
+                        stdout=PIPE,
+                        stderr=PIPE,
+                    ) as process:
+                        mimetype, _ = process.communicate()
                     self._mimetype = mimetype.decode(ENCODING).strip()
                 except OSError:
                     pass
@@ -437,8 +445,10 @@ class Rifle(object):  # pylint: disable=too-many-instance-attributes
                 if 'f' in flags or 't' in flags:
                     Popen_forked(cmd, env=self.hook_environment(os.environ))
                 else:
-                    process = Popen(cmd, env=self.hook_environment(os.environ))
-                    process.wait()
+                    with Popen(
+                        cmd, env=self.hook_environment(os.environ)
+                    ) as process:
+                        process.wait()
             finally:
                 self.hook_after_executing(command, self._mimetype, self._app_flags)
 
@@ -518,8 +528,8 @@ def main():  # pylint: disable=too-many-locals
         label = options.p
 
     if options.w is not None and not options.l:
-        process = Popen([options.w] + list(positional))
-        process.wait()
+        with Popen([options.w] + list(positional)) as process:
+            process.wait()
     else:
         # Start up rifle
         rifle = Rifle(conf_path)
diff --git a/ranger/ext/spawn.py b/ranger/ext/spawn.py
index dacb3c4a..d4590c48 100644
--- a/ranger/ext/spawn.py
+++ b/ranger/ext/spawn.py
@@ -28,12 +28,12 @@ def check_output(popenargs, **kwargs):
     kwargs.setdefault('shell', isinstance(popenargs, str))
 
     if 'stderr' in kwargs:
-        process = Popen(popenargs, **kwargs)
-        stdout, _ = process.communicate()
+        with Popen(popenargs, **kwargs) as process:
+            stdout, _ = process.communicate()
     else:
         with open(devnull, mode='w') as fd_devnull:
-            process = Popen(popenargs, stderr=fd_devnull, **kwargs)
-            stdout, _ = process.communicate()
+            with Popen(popenargs, stderr=fd_devnull, **kwargs) as process:
+                stdout, _ = process.communicate()
 
     if process.returncode != 0:
         error = CalledProcessError(process.returncode, popenargs)
diff --git a/ranger/ext/vcs/vcs.py b/ranger/ext/vcs/vcs.py
index bd744458..89259b5e 100644
--- a/ranger/ext/vcs/vcs.py
+++ b/ranger/ext/vcs/vcs.py
@@ -87,6 +87,7 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
         if self.root:
             if self.is_root:
                 self.rootvcs = self
+                # pylint: disable=invalid-class-object
                 self.__class__ = globals()[self.REPOTYPES[self.repotype]['class'] + 'Root']
 
                 if not os.access(self.repodir, os.R_OK):
@@ -100,6 +101,7 @@ class Vcs(object):  # pylint: disable=too-many-instance-attributes
                 if self.rootvcs is None or self.rootvcs.root is None:
                     return
                 self.rootvcs.links |= self.links
+                # pylint: disable=invalid-class-object
                 self.__class__ = globals()[self.REPOTYPES[self.repotype]['class']]
                 self.track = self.rootvcs.track
 
diff --git a/ranger/gui/colorscheme.py b/ranger/gui/colorscheme.py
index 01862957..0e68c5ff 100644
--- a/ranger/gui/colorscheme.py
+++ b/ranger/gui/colorscheme.py
@@ -108,7 +108,9 @@ def _colorscheme_name_to_class(signal):  # pylint: disable=too-many-branches
         if os.path.exists(signal.fm.confpath('colorschemes')):
             initpy = signal.fm.confpath('colorschemes', '__init__.py')
             if not os.path.exists(initpy):
-                open(initpy, 'a').close()
+                with open(initpy, "a"):
+                    # Just create the file
+                    pass
 
     if usecustom and \
             exists(signal.fm.confpath('colorschemes', scheme_name)):
diff --git a/ranger/gui/widgets/console.py b/ranger/gui/widgets/console.py
index 71c8b6ed..02676f45 100644
--- a/ranger/gui/widgets/console.py
+++ b/ranger/gui/widgets/console.py
@@ -44,17 +44,20 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
             self.historypath = self.fm.datapath('history')
             if os.path.exists(self.historypath):
                 try:
-                    fobj = open(self.historypath, 'r')
-                except OSError as ex:
-                    self.fm.notify('Failed to read history file', bad=True, exception=ex)
-                else:
-                    try:
-                        for line in fobj:
-                            self.history.add(line[:-1])
-                    except UnicodeDecodeError as ex:
-                        self.fm.notify('Failed to parse corrupt history file',
-                                       bad=True, exception=ex)
-                    fobj.close()
+                    with open(self.historypath, "r") as fobj:
+                        try:
+                            for line in fobj:
+                                self.history.add(line[:-1])
+                        except UnicodeDecodeError as ex:
+                            self.fm.notify(
+                                "Failed to parse corrupt history file",
+                                bad=True,
+                                exception=ex,
+                            )
+                except (OSError, IOError) as ex:
+                    self.fm.notify(
+                        "Failed to read history file", bad=True, exception=ex
+                    )
         self.history_backup = History(self.history)
 
         # NOTE: the console is considered in the "question mode" when the
@@ -76,16 +79,16 @@ class Console(Widget):  # pylint: disable=too-many-instance-attributes,too-many-
             return
         if self.historypath:
             try:
-                fobj = open(self.historypath, 'w')
-            except OSError as ex:
-                self.fm.notify('Failed to write history file', bad=True, exception=ex)
-            else:
-                for entry in self.history_backup:
-                    try:
-                        fobj.write(entry + '\n')
-                    except UnicodeEncodeError:
-                        pass
-                fobj.close()
+                with open(self.historypath, 'w') as fobj:
+                    for entry in self.history_backup:
+                        try:
+                            fobj.write(entry + '\n')
+                        except UnicodeEncodeError:
+                            pass
+            except (OSError, IOError) as ex:
+                self.fm.notify(
+                    "Failed to write history file", bad=True, exception=ex
+                )
         Widget.destroy(self)
 
     def _calculate_offset(self):
diff --git a/tests/manpage_completion_test.py b/tests/manpage_completion_test.py
index b9504d06..f5e5c335 100755
--- a/tests/manpage_completion_test.py
+++ b/tests/manpage_completion_test.py
@@ -26,7 +26,8 @@ def get_path_of_man_page():
 
 def read_manpage():
     path = get_path_of_man_page()
-    return open(path, 'r').read()
+    with open(path, 'r') as man_page:
+        return man_page.read()
 
 
 def get_sections():