diff options
author | toonn <toonn@toonn.io> | 2020-07-05 15:45:39 +0200 |
---|---|---|
committer | toonn <toonn@toonn.io> | 2020-07-05 15:45:39 +0200 |
commit | 710156c371ac726257f1068546b0da889a26494c (patch) | |
tree | 8fe98f4c1c56be19f75c475d1e3b1fec5972ce76 | |
parent | aff694730d3df63778923e3f59cc77ff58c9235a (diff) | |
parent | dfa60b5a8ed179cda8e262b98739913544278e46 (diff) | |
download | ranger-710156c371ac726257f1068546b0da889a26494c.tar.gz |
Merge branch 'pylint3k'
-rw-r--r-- | .github/workflows/doctest.yml | 6 | ||||
-rw-r--r-- | .github/workflows/py37.yml | 30 | ||||
-rw-r--r-- | .github/workflows/python.yml | 4 | ||||
-rw-r--r-- | .pylintrc | 7 | ||||
-rw-r--r-- | .travis.yml | 17 | ||||
-rw-r--r-- | ranger/config/.pylintrc | 2 | ||||
-rwxr-xr-x | ranger/config/commands.py | 6 | ||||
-rw-r--r-- | ranger/container/directory.py | 3 | ||||
-rw-r--r-- | ranger/container/fsobject.py | 6 | ||||
-rw-r--r-- | ranger/container/tags.py | 10 | ||||
-rw-r--r-- | ranger/core/actions.py | 8 | ||||
-rw-r--r-- | ranger/core/fm.py | 12 | ||||
-rw-r--r-- | ranger/core/main.py | 1 | ||||
-rw-r--r-- | ranger/ext/img_display.py | 9 | ||||
-rw-r--r-- | ranger/ext/macrodict.py | 1 | ||||
-rw-r--r-- | ranger/ext/safe_path.py | 1 | ||||
-rw-r--r-- | ranger/ext/shell_escape.py | 2 | ||||
-rw-r--r-- | ranger/ext/vcs/vcs.py | 10 | ||||
-rw-r--r-- | ranger/gui/ansi.py | 24 | ||||
-rw-r--r-- | ranger/gui/displayable.py | 7 | ||||
-rw-r--r-- | ranger/gui/ui.py | 4 | ||||
-rw-r--r-- | ranger/gui/widgets/browsercolumn.py | 2 | ||||
-rw-r--r-- | requirements.txt | 2 | ||||
-rw-r--r-- | tests/pylint/py2_compat.py | 126 | ||||
-rw-r--r-- | tests/pylint/test_py2_compat.py | 157 | ||||
-rw-r--r-- | tests/ranger/core/test_main.py | 1 |
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 |