From 33d2511719bdb526d426d26d84763b8cda39f9a2 Mon Sep 17 00:00:00 2001 From: Vladimir Rudnyh Date: Tue, 2 Feb 2016 18:34:20 +0300 Subject: [PATCH] Add new linting tool: flake8-import-order 0.6.1 --- Flake8Lint.py | 15 +- Flake8Lint.sublime-settings | 5 + README.md | 19 +- contrib/flake8_import_order/__about__.py | 32 ++ contrib/flake8_import_order/__init__.py | 329 +++++++++++++++++++ contrib/flake8_import_order/flake8_linter.py | 52 +++ contrib/flake8_import_order/pylama_linter.py | 35 ++ contrib/flake8_import_order/stdlib_list.py | 326 ++++++++++++++++++ lint.py | 70 +++- 9 files changed, 868 insertions(+), 15 deletions(-) create mode 100755 contrib/flake8_import_order/__about__.py create mode 100755 contrib/flake8_import_order/__init__.py create mode 100755 contrib/flake8_import_order/flake8_linter.py create mode 100755 contrib/flake8_import_order/pylama_linter.py create mode 100755 contrib/flake8_import_order/stdlib_list.py diff --git a/Flake8Lint.py b/Flake8Lint.py index bb640f0..4927abb 100644 --- a/Flake8Lint.py +++ b/Flake8Lint.py @@ -5,6 +5,7 @@ Check Python files with flake8 (PEP8, pyflake and mccabe) """ from __future__ import print_function + import fnmatch import itertools import os @@ -44,8 +45,8 @@ PROJECT_SETTINGS_KEYS = ( 'python_interpreter', 'builtins', 'pyflakes', 'pep8', 'pydocstyle', - 'naming', 'debugger', 'complexity', 'pep8_max_line_length', - 'select', 'ignore', 'ignore_files', + 'naming', 'debugger', 'import_order', 'import_order_style', 'complexity', + 'pep8_max_line_length', 'select', 'ignore', 'ignore_files', 'use_flake8_global_config', 'use_flake8_project_config', ) FLAKE8_SETTINGS_KEYS = ( @@ -204,6 +205,16 @@ def setup(self): # turn on flake8-debugger error lint self.debugger = bool(self.settings.get('debugger', True)) + # turn on import order error lint + self.import_order = bool(self.settings.get('import-order', True)) + + # get import order style + import_order_style = self.settings.get('import-order-style') + if import_order_style in ('cryptography', 'google'): + self.import_order_style = import_order_style + else: + self.import_order_style = 'cryptography' + # turn off complexity check (set number > 0 to check complexity level) try: self.complexity = int(self.settings.get('complexity', -1)) diff --git a/Flake8Lint.sublime-settings b/Flake8Lint.sublime-settings index 79e3aa5..453d36d 100644 --- a/Flake8Lint.sublime-settings +++ b/Flake8Lint.sublime-settings @@ -71,6 +71,11 @@ "naming": true, // turn on debugger error lint "debugger": true, + // turn on import order error lint + "import-order": false, + // import order style: "cryptography" or "google" + // See also: https://github.com/public/flake8-import-order#configuration + "import-order-style": "cryptography", // turn off complexity check (set number > 0 to check complexity level) "complexity": -1, diff --git a/README.md b/README.md index 59c0902..59d87a3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Python Flake8 Lint ================== -Python Flake8 Lint is a Sublime Text 2/3 plugin for check Python files against some of the style conventions in **[PEP8](http://www.python.org/dev/peps/pep-0008/)**, **[pydocstyle](https://github.com/PyCQA/pydocstyle)**, **[PyFlakes](https://launchpad.net/pyflakes)**, **[mccabe](http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html)** and **[pep8-naming](https://github.com/flintwork/pep8-naming)**. +Python Flake8 Lint is a Sublime Text 2/3 plugin for check Python files against some of the style conventions in **[PEP8](http://www.python.org/dev/peps/pep-0008/)**, **[pydocstyle](https://github.com/PyCQA/pydocstyle)**, **[PyFlakes](https://launchpad.net/pyflakes)**, **[mccabe](http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html)**, **[pep8-naming](https://github.com/flintwork/pep8-naming)**, **[flake8-debugger](https://github.com/JBKahn/flake8-debugger)** and **[flake8-import-order](https://github.com/public/flake8-import-order)**. Based on **[bitbucket.org/tarek/flake8](https://bitbucket.org/tarek/flake8)**. @@ -25,6 +25,8 @@ There are additional tools used to lint Python files: * **[flake8-debugger](https://github.com/JBKahn/flake8-debugger)** is a flake8 debug statement checker. +* **[flake8-import-order](https://github.com/public/flake8-import-order)** is a flake8 plugin that checks import order in the fashion of the Google Python Style Guide (turned off by default). + Install ------- @@ -127,6 +129,11 @@ Default "Python Flake8 Lint" plugin config: Preferences->Package "naming": true, // turn on debugger error lint "debugger": true, + // turn on import order error lint + "import-order": false, + // import order style: "cryptography" or "google" + // See also: https://github.com/public/flake8-import-order#configuration + "import-order-style": "cryptography", // turn off complexity check (set number > 0 to check complexity level) "complexity": -1, @@ -190,6 +197,8 @@ You could define per-project config for "Python Flake8 Lint". Use ProjectProject 0: + return IMPORT_APP_RELATIVE + + elif pkg in STDLIB_NAMES: + return IMPORT_STDLIB + + else: + # Not future, stdlib or an application import. + # Must be 3rd party. + return IMPORT_3RD_PARTY + + +class ImportOrderChecker(object): + visitor_class = ImportVisitor + options = None + + def __init__(self, filename, tree): + self.tree = tree + self.filename = filename + self.lines = None + + def load_file(self): + if self.filename in ("stdin", "-", None): + self.filename = "stdin" + self.lines = pep8.stdin_get_value().splitlines(True) + else: + self.lines = pep8.readlines(self.filename) + + if not self.tree: + self.tree = ast.parse("".join(self.lines)) + + def error(self, node, code, message): + raise NotImplemented() + + def check_order(self): + if not self.tree or not self.lines: + self.load_file() + + visitor = self.visitor_class(self.filename, self.options) + visitor.visit(self.tree) + + style = self.options['import_order_style'] + + prev_node = None + for node in visitor.imports: + # Lines with the noqa flag are ignored entirely + if pep8.noqa(self.lines[node.lineno - 1]): + continue + + n, k = visitor.node_sort_key(node) + + if style == "google": + cmp_n = cmp_values(n, style) + else: + cmp_n = n + + if cmp_n[-1] and not is_sorted(cmp_n[-1]): + sort_key = lambda s: s[0] + if style == "google": + sort_key = lambda s: s[0].lower() + should_be = ", ".join( + name[0] for name in + sorted(n[-1], key=sort_key)) + yield self.error( + node, "I101", + ( + "Imported names are in the wrong order. " + "Should be {0}".format(should_be) + ) + ) + + if prev_node is None: + prev_node = node + continue + + pn, pk = visitor.node_sort_key(prev_node) + + if style == "google": + cmp_pn = cmp_values(pn, style) + else: + cmp_pn = pn + + # FUTURES + # STDLIBS, STDLIB_FROMS + # 3RDPARTY[n], 3RDPARTY_FROM[n] + # 3RDPARTY[n+1], 3RDPARTY_FROM[n+1] + # APPLICATION, APPLICATION_FROM + + # import_type, names, level, is_star_import, imported_names, + + if n[0] == IMPORT_MIXED: + yield self.error( + node, "I666", + "Import statement mixes groups" + ) + prev_node = node + continue + + if cmp_n < cmp_pn: + def build_str(key): + level = key[2] + if level >= 0: + start = "from " + level * '.' + else: + start = "import " + return start + ", ".join(key[1]) + + first_str = build_str(k) + second_str = build_str(pk) + + yield self.error( + node, "I100", + ( + "Imports statements are in the wrong order. " + "{0} should be before {1}".format( + first_str, + second_str + ) + ) + ) + + lines_apart = node.lineno - prev_node.lineno + + is_app = ( + set([cmp_n[0], cmp_pn[0]]) != + set([IMPORT_APP, IMPORT_APP_RELATIVE]) + ) + + if lines_apart == 1 and (( + cmp_n[0] != cmp_pn[0] and + (style != "google" or is_app) + ) or ( + n[0] == IMPORT_3RD_PARTY and + style != 'google' and + root_package_name(cmp_n[1][0]) != + root_package_name(cmp_pn[1][0]) + )): + yield self.error( + node, "I201", + "Missing newline before sections or imports." + ) + + prev_node = node diff --git a/contrib/flake8_import_order/flake8_linter.py b/contrib/flake8_import_order/flake8_linter.py new file mode 100755 index 0000000..d3c0eab --- /dev/null +++ b/contrib/flake8_import_order/flake8_linter.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import + +import flake8_import_order +from flake8_import_order import DEFAULT_IMPORT_ORDER_STYLE, ImportOrderChecker + + +class Linter(ImportOrderChecker): + name = "import-order" + version = flake8_import_order.__version__ + + def __init__(self, tree, filename): + super(Linter, self).__init__(filename, tree) + + @classmethod + def add_options(cls, parser): + # List of application import names. They go last. + parser.add_option( + "--application-import-names", + default="", + action="store", + type="string", + help="Import names to consider as application specific" + ) + parser.add_option( + "--import-order-style", + default=DEFAULT_IMPORT_ORDER_STYLE, + action="store", + type="string", + help="Style to follow. Available: cryptography, google" + ) + parser.config_options.append("application-import-names") + parser.config_options.append("import-order-style") + + @classmethod + def parse_options(cls, options): + optdict = {} + + names = options.application_import_names.split(",") + optdict = dict( + application_import_names=[n.strip() for n in names], + import_order_style=options.import_order_style, + ) + + cls.options = optdict + + def error(self, node, code, message): + lineno, col_offset = node.lineno, node.col_offset + return (lineno, col_offset, '{0} {1}'.format(code, message), Linter) + + def run(self): + for error in self.check_order(): + yield error diff --git a/contrib/flake8_import_order/pylama_linter.py b/contrib/flake8_import_order/pylama_linter.py new file mode 100755 index 0000000..286468c --- /dev/null +++ b/contrib/flake8_import_order/pylama_linter.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import + +from pylama.lint import Linter as BaseLinter + +from flake8_import_order import DEFAULT_IMPORT_ORDER_STYLE, ImportOrderChecker + + +class Linter(ImportOrderChecker, BaseLinter): + name = "import-order" + version = "0.1" + + def __init__(self): + super(Linter, self).__init__(None, None) + + def allow(self, path): + return path.endswith(".py") + + def error(self, node, code, message): + lineno, col_offset = node.lineno, node.col_offset + return { + "lnum": lineno, + "col": col_offset, + "text": message, + "type": code + } + + def run(self, path, **meta): + self.filename = path + self.tree = None + self.options = dict( + {'import_order_style': DEFAULT_IMPORT_ORDER_STYLE}, + **meta) + + for error in self.check_order(): + yield error diff --git a/contrib/flake8_import_order/stdlib_list.py b/contrib/flake8_import_order/stdlib_list.py new file mode 100755 index 0000000..3724ada --- /dev/null +++ b/contrib/flake8_import_order/stdlib_list.py @@ -0,0 +1,326 @@ +STDLIB_NAMES = set(( + "AL", + "BaseHTTPServer", + "Bastion", + "Binary", + "Boolean", + "CGIHTTPServer", + "ColorPicker", + "ConfigParser", + "Cookie", + "DEVICE", + "DocXMLRPCServer", + "EasyDialogs", + "FL", + "FrameWork", + "GL", + "HTMLParser", + "MacOS", + "Mapping", + "MimeWriter", + "MiniAEFrame", + "Numeric", + "Queue", + "SUNAUDIODEV", + "ScrolledText", + "Sequence", + "Set", + "SimpleHTTPServer", + "SimpleXMLRPCServer", + "SocketServer", + "StringIO", + "Text", + "Tix", + "Tkinter", + "UserDict", + "UserList", + "UserString", + "__builtin__", + "__future__", + "__main__", + "_dummy_thread", + "_thread", + "abc", + "aepack", + "aetools", + "aetypes", + "aifc", + "al", + "anydbm", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "autoGIL", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "bsddb", + "builtins", + "bz2", + "cPickle", + "cStringIO", + "calendar", + "cd", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "collections.abc", + "colorsys", + "commands", + "compileall", + "concurrent.futures", + "configparser", + "contextlib", + "cookielib", + "copy", + "copy_reg", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "curses.ascii", + "curses.panel", + "curses.textpad", + "curses.wrapper", + "datetime", + "dbhash", + "dbm", + "decimal", + "difflib", + "dircache", + "dis", + "distutils", + "dl", + "doctest", + "dumbdbm", + "dummy_thread", + "dummy_threading", + "email", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "findertools", + "fl", + "flp", + "fm", + "fnmatch", + "formatter", + "fpectl", + "fpformat", + "fractions", + "ftplib", + "functools", + "future_builtins", + "gc", + "gdbm", + "gensuitemodule", + "getopt", + "getpass", + "gettext", + "gl", + "glob", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "hotshot", + "html", + "html.entities", + "html.parser", + "htmlentitydefs", + "htmllib", + "http", + "http.client", + "http.cookiejar", + "http.cookies", + "http.server", + "httplib", + "ic", + "imageop", + "imaplib", + "imgfile", + "imghdr", + "imp", + "importlib", + "imputil", + "inspect", + "io", + "ipaddress", + "itertools", + "jpeg", + "json", + "keyword", + "linecache", + "locale", + "logging", + "logging.config", + "logging.handlers", + "lzma", + "macostools", + "macpath", + "macurl2path", + "mailbox", + "mailcap", + "marshal", + "math", + "md5", + "mhlib", + "mimetools", + "mimetypes", + "mimify", + "mmap", + "modulefinder", + "msilib", + "multifile", + "multiprocessing", + "mutex", + "netrc", + "new", + "nis", + "nntplib", + "nturl2path", + "numbers", + "operator", + "optparse", + "os", + "os.path", + "ossaudiodev", + "parser", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "popen2", + "poplib", + "posix", + "posixfile", + "posixpath", + "pprint", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "repr", + "reprlib", + "resource", + "rexec", + "rfc822", + "rlcompleter", + "robotparser", + "runpy", + "sched", + "select", + "sets", + "sgmllib", + "sha", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "statvfs", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "sunaudiodev", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "test", + "test.support", + "test.test_support", + "textwrap", + "thread", + "threading", + "time", + "timeit", + "tkinter", + "tkinter.scrolledtext", + "tkinter.tix", + "tkinter.ttk", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "ttk", + "tty", + "turtle", + "types", + "unicodedata", + "unittest", + "unittest.mock", + "urllib", + "urllib.error", + "urllib.parse", + "urllib.request", + "urllib.response", + "urllib.robotparser", + "urllib2", + "urlparse", + "user", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "whichdb", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpclib", + "zipfile", + "zipimport", + "zlib", +)) diff --git a/lint.py b/lint.py index 7da563b..09e0683 100644 --- a/lint.py +++ b/lint.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """Flake8 lint worker.""" from __future__ import print_function + +import ast import os import sys @@ -20,12 +22,16 @@ if CONTRIB_PATH not in sys.path: sys.path.insert(0, CONTRIB_PATH) -import ast +from flake8 import __version__ as flake8_version +from flake8._pyflakes import patch_pyflakes +import flake8_debugger +from flake8_import_order import ( + __version__ as flake8_import_order_version, + ImportOrderChecker +) import mccabe import pep8 import pep8ext_naming -import flake8_debugger -import pyflakes.api from pydocstyle import ( __version__ as pydocstyle_version, PEP257Checker @@ -34,10 +40,9 @@ __version__ as pyflakes_version, checker as pyflakes_checker ) -from flake8 import __version__ as flake8_version -from flake8._pyflakes import patch_pyflakes -patch_pyflakes() +import pyflakes.api +patch_pyflakes() if sys.platform.startswith('win'): DEFAULT_CONFIG_FILE = os.path.expanduser(r'~\.flake8') @@ -57,8 +62,9 @@ def tools_versions(): ('pyflakes', pyflakes_version), ('mccabe', mccabe.__version__), ('pydocstyle', pydocstyle_version), - ('pep8-naming', pep8ext_naming.__version__), - ('flake8-debugger', flake8_debugger.__version__), + ('naming', pep8ext_naming.__version__), + ('debugger', flake8_debugger.__version__), + ('import-order', flake8_import_order_version), ) @@ -114,6 +120,32 @@ def flake(self, msg): ) +class ImportOrderLinter(ImportOrderChecker): + """Import order linter.""" + + def __init__(self, tree, filename, lines, order_style='cryptography'): + """Initialize linter.""" + super(ImportOrderLinter, self).__init__(filename, tree) + self.lines = lines + self.options = { + 'import_order_style': order_style, + } + + def load_file(self): + """Load file.""" + pass + + def error(self, node, code, message): + """Format lint error.""" + lineno, col_offset = node.lineno, node.col_offset + return (lineno, col_offset, '{0} {1}'.format(code, message)) + + def run(self): + """Run lint.""" + for error in self.check_order(): + yield error + + def load_flake8_config(filename, global_config=False, project_config=False): """Return flake8 settings from config file. @@ -227,6 +259,13 @@ def lint(lines, settings): (warn.get("line"), warn.get("col"), warn.get("message")) ) + # lint with import order + if settings.get('import-order', True): + order_style = settings.get('import_order_style') + import_linter = ImportOrderLinter(tree, None, lines, order_style) + for error in import_linter.run(): + warnings.append(error[0:3]) + # check complexity try: complexity = int(settings.get('complexity', -1)) @@ -276,6 +315,17 @@ def lint_external(lines, settings, interpreter, linter): if settings.get('debugger', True): arguments.append('--debugger') + # do we need to run import order lint + if settings.get('import_order', True): + arguments.append('--import-order') + + # get import order style + import_order_style = settings.get('import_order_style') + if import_order_style in ('cryptography', 'google'): + arguments.extend(('--import-order-style', import_order_style)) + else: + arguments.extend(('--import-order-style', 'cryptography')) + # do we need to run complexity check complexity = settings.get('complexity', -1) arguments.extend(('--complexity', str(complexity))) @@ -332,6 +382,10 @@ def lint_external(lines, settings, interpreter, linter): help="run naming lint") arg_parser.add_argument('--debugger', action='store_true', help="run debugger lint") + arg_parser.add_argument('--import-order', action='store_true', + help="run import order lint") + arg_parser.add_argument('--import-order-style', + help="import order style: cryptography or google") arg_parser.add_argument('--complexity', type=int, help="check complexity") arg_parser.add_argument('--pep8-max-line-length', type=int, default=79,