diff options
-rw-r--r-- | .github/workflows/pylint.yml (renamed from .github/workflows/doctest.yml) | 20 | ||||
-rw-r--r-- | .github/workflows/pypy.yml | 30 | ||||
-rw-r--r-- | .github/workflows/python.yml | 18 | ||||
-rw-r--r-- | .github/workflows/shellcheck.yml | 4 | ||||
-rw-r--r-- | .travis.yml | 20 | ||||
-rw-r--r-- | ranger/__init__.py | 16 | ||||
-rwxr-xr-x | ranger/config/commands.py | 29 | ||||
-rw-r--r-- | ranger/container/bookmarks.py | 27 | ||||
-rw-r--r-- | ranger/container/history.py | 10 | ||||
-rw-r--r-- | ranger/container/tags.py | 18 | ||||
-rw-r--r-- | ranger/core/actions.py | 9 | ||||
-rw-r--r-- | ranger/core/loader.py | 6 | ||||
-rw-r--r-- | ranger/core/main.py | 20 | ||||
-rw-r--r-- | ranger/core/runner.py | 3 | ||||
-rw-r--r-- | ranger/ext/accumulator.py | 3 | ||||
-rw-r--r-- | ranger/ext/img_display.py | 74 | ||||
-rw-r--r-- | ranger/ext/macrodict.py | 2 | ||||
-rw-r--r-- | ranger/ext/open23.py | 48 | ||||
-rw-r--r-- | ranger/ext/popen_forked.py | 9 | ||||
-rwxr-xr-x | ranger/ext/rifle.py | 62 | ||||
-rw-r--r-- | ranger/ext/spawn.py | 8 | ||||
-rw-r--r-- | ranger/ext/vcs/vcs.py | 2 | ||||
-rw-r--r-- | ranger/gui/colorscheme.py | 4 | ||||
-rw-r--r-- | ranger/gui/widgets/console.py | 45 | ||||
-rwxr-xr-x | tests/manpage_completion_test.py | 3 |
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(): |