diff --git a/bin/pylama/__init__.py b/bin/pylama/__init__.py index 09d5d4c..e529c18 100644 --- a/bin/pylama/__init__.py +++ b/bin/pylama/__init__.py @@ -4,7 +4,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = "7.4.3" +__version__ = "7.6.6" __project__ = "pylama" __author__ = "Kirill Klenov " __license__ = "GNU LGPL" diff --git a/bin/pylama/async.py b/bin/pylama/check_async.py similarity index 100% rename from bin/pylama/async.py rename to bin/pylama/check_async.py diff --git a/bin/pylama/config.py b/bin/pylama/config.py index 6b2bb22..65aa8f8 100644 --- a/bin/pylama/config.py +++ b/bin/pylama/config.py @@ -12,7 +12,7 @@ from .lint.extensions import LINTERS #: A default checkers -DEFAULT_LINTERS = 'pycodestyle', 'pyflakes', 'mccabe' +DEFAULT_LINTERS = 'pycodestyle', 'pyflakes', 'mccabe', 'eradicate' CURDIR = os.getcwd() CONFIG_FILES = 'pylama.ini', 'setup.cfg', 'tox.ini', 'pytest.ini' @@ -21,7 +21,9 @@ SKIP_PATTERN = re.compile(r'# *noqa\b', re.I).search # Parse a modelines -MODELINE_RE = re.compile(r'^\s*#\s+(?:pylama:)\s*((?:[\w_]*=[^:\n\s]+:?)+)', re.I | re.M) +MODELINE_RE = re.compile( + r'^\s*#\s+(?:pylama:)\s*((?:[\w_]*=[^:\n\s]+:?)+)', + re.I | re.M) # Setup a logger LOGGER = logging.getLogger('pylama') @@ -46,7 +48,6 @@ def split_csp_str(val): """ Split comma separated string into unique values, keeping their order. :returns: list of splitted values - """ seen = set() values = val if isinstance(val, (list, tuple)) else val.strip().split(',') @@ -111,7 +112,7 @@ def get_default_config_file(rootdir=None): "--linters", "-l", default=_Default(','.join(DEFAULT_LINTERS)), type=parse_linters, help=( "Select linters. (comma-separated). Choices are %s." - % ','.join(s for s in LINTERS.keys()) + % ','.join(s for s in LINTERS) )) PARSER.add_argument( @@ -120,7 +121,8 @@ def get_default_config_file(rootdir=None): PARSER.add_argument( "--skip", default=_Default(''), - type=lambda s: [re.compile(fnmatch.translate(p)) for p in s.split(',') if p], + type=lambda s: [re.compile(fnmatch.translate(p)) + for p in s.split(',') if p], help="Skip files by masks (comma-separated, Ex. */messages.py)") PARSER.add_argument("--report", "-r", help="Send report to file [REPORT]") @@ -128,7 +130,7 @@ def get_default_config_file(rootdir=None): "--hook", action="store_true", help="Install Git (Mercurial) hook.") PARSER.add_argument( - "--async", action="store_true", + "--concurrent", "--async", action="store_true", help="Enable async mode. Useful for checking a lot of files. " "Unsupported with pylint.") @@ -148,10 +150,11 @@ def get_default_config_file(rootdir=None): help="Use absolute paths in output.") -ACTIONS = dict((a.dest, a) for a in PARSER._actions) # pylint: disable=protected-access +ACTIONS = dict((a.dest, a) + for a in PARSER._actions) # pylint: disable=protected-access -def parse_options(args=None, config=True, rootdir=CURDIR, **overrides): # noqa +def parse_options(args=None, config=True, rootdir=CURDIR, **overrides): # noqa """ Parse options from command line and configuration files. :return argparse.Namespace: @@ -173,21 +176,24 @@ def parse_options(args=None, config=True, rootdir=CURDIR, **overrides): # noqa if isinstance(passed_value, _Default): if opt == 'paths': val = val.split() + if opt == 'skip': + val = fix_pathname_sep(val) setattr(options, opt, _Default(val)) # Parse file related options for name, opts in cfg.sections.items(): - if not name.startswith('pylama') or name == cfg.default_section: + if name == cfg.default_section: continue - name = name[7:] + if name.startswith('pylama'): + name = name[7:] if name in LINTERS: options.linters_params[name] = dict(opts) continue - mask = re.compile(fnmatch.translate(name)) + mask = re.compile(fnmatch.translate(fix_pathname_sep(name))) options.file_params[mask] = dict(opts) # Override options @@ -199,9 +205,9 @@ def parse_options(args=None, config=True, rootdir=CURDIR, **overrides): # noqa if isinstance(value, _Default): setattr(options, name, process_value(name, value.value)) - if options.async and 'pylint' in options.linters: + if options.concurrent and 'pylint' in options.linters: LOGGER.warning('Can\'t parse code asynchronously with pylint enabled.') - options.async = False + options.concurrent = False return options @@ -242,7 +248,7 @@ def get_config(ini_path=None, rootdir=None): config = Namespace() config.default_section = 'pylama' - if not ini_path or ini_path == 'None': + if not ini_path: path = get_default_config_file(rootdir) if path: config.read(path) @@ -260,6 +266,11 @@ def setup_logger(options): LOGGER.addHandler(logging.FileHandler(options.report, mode='w')) if options.options: - LOGGER.info('Try to read configuration from: ' + options.options) + LOGGER.info('Try to read configuration from: %r', options.options) + + +def fix_pathname_sep(val): + """Fix pathnames for Win.""" + return val.replace(os.altsep or "\\", os.sep) # pylama:ignore=W0212,D210,F0001 diff --git a/bin/pylama/core.py b/bin/pylama/core.py index c526fa9..ddf3892 100644 --- a/bin/pylama/core.py +++ b/bin/pylama/core.py @@ -50,7 +50,7 @@ def run(path='', code=None, rootdir=CURDIR, options=None): for item in params.get('linters') or linters: if not isinstance(item, tuple): - item = (item, LINTERS.get(item)) + item = item, LINTERS.get(item) lname, linter = item @@ -60,39 +60,46 @@ def run(path='', code=None, rootdir=CURDIR, options=None): lparams = linters_params.get(lname, dict()) LOGGER.info("Run %s %s", lname, lparams) + ignore, select = merge_params(params, lparams) + linter_errors = linter.run( - path, code=code, ignore=params.get("ignore", set()), - select=params.get("select", set()), params=lparams) - if linter_errors: - for er in linter_errors: - errors.append(Error(filename=path, linter=lname, **er)) + path, code=code, ignore=ignore, select=select, params=lparams) + if not linter_errors: + continue + + errors += filter_errors([ + Error(filename=path, linter=lname, **er) for er in linter_errors + ], ignore=ignore, select=select) except IOError as e: - LOGGER.debug("IOError %s", e) + LOGGER.error("IOError %s", e) errors.append(Error(text=str(e), filename=path, linter=lname)) except SyntaxError as e: - LOGGER.debug("SyntaxError %s", e) + LOGGER.error("SyntaxError %s", e) errors.append( Error(linter='pylama', lnum=e.lineno, col=e.offset, text='E0100 SyntaxError: {}'.format(e.args[0]), filename=path)) - except Exception as e: # noqa + except Exception as e: # noqa import traceback - LOGGER.info(traceback.format_exc()) - - errors = filter_errors(errors, **params) # noqa + LOGGER.error(traceback.format_exc()) errors = list(remove_duplicates(errors)) if code and errors: errors = filter_skiplines(code, errors) - key = lambda e: e.lnum if options and options.sort: sort = dict((v, n) for n, v in enumerate(options.sort, 1)) - key = lambda e: (sort.get(e.type, 999), e.lnum) + + def key(e): + return (sort.get(e.type, 999), e.lnum) + else: + def key(e): + return e.lnum + return sorted(errors, key=key) @@ -124,6 +131,8 @@ def prepare_params(modeline, fileconfig, options): for key in ('ignore', 'select', 'linters'): params[key] += process_value(key, config.get(key, [])) params['skip'] = bool(int(config.get('skip', False))) + # TODO: skip what? This is causing erratic behavior for linters. + params['skip'] = False params['ignore'] = set(params['ignore']) params['select'] = set(params['select']) @@ -174,6 +183,19 @@ def filter_skiplines(code, errors): return errors +def merge_params(params, lparams): + """Merge global ignore/select with linter local params.""" + ignore = params.get('ignore', set()) + if 'ignore' in lparams: + ignore = ignore | set(lparams['ignore']) + + select = params.get('select', set()) + if 'select' in lparams: + select = select | set(lparams['select']) + + return ignore, select + + class CodeContext(object): """Read file if code is None. """ diff --git a/bin/pylama/lint/extensions.py b/bin/pylama/lint/extensions.py index 9c2c65d..ad3f6ef 100644 --- a/bin/pylama/lint/extensions.py +++ b/bin/pylama/lint/extensions.py @@ -1,13 +1,5 @@ """Load extensions.""" -import os -import sys - -CURDIR = os.path.dirname(__file__) -sys.path.insert( - 0, os.path.abspath(os.path.join(CURDIR, '..', '..', 'deps'))) - - LINTERS = {} try: @@ -16,6 +8,12 @@ except ImportError: pass +try: + from pylama.lint.pylama_eradicate import Linter + LINTERS['eradicate'] = Linter() +except ImportError: + pass + try: from pylama.lint.pylama_pydocstyle import Linter LINTERS['pep257'] = Linter() # for compatibility @@ -48,10 +46,14 @@ except ImportError: pass -try: - from isort.pylama_isort import Linter - LINTERS['isort'] = Linter() -except ImportError: - pass + +from pkg_resources import iter_entry_points + +for entry in iter_entry_points('pylama.linter'): + if entry.name not in LINTERS: + try: + LINTERS[entry.name] = entry.load()() + except ImportError: + pass # pylama:ignore=E0611 diff --git a/bin/pylama/lint/pylama_eradicate.py b/bin/pylama/lint/pylama_eradicate.py new file mode 100644 index 0000000..4e84b0e --- /dev/null +++ b/bin/pylama/lint/pylama_eradicate.py @@ -0,0 +1,36 @@ +"""Commented-out code checking.""" +from eradicate import commented_out_code_line_numbers +from pylama.lint import Linter as Abstract + +try: + converter = unicode +except NameError: + converter = str + + +class Linter(Abstract): + + """Run commented-out code checking.""" + + @staticmethod + def run(path, code=None, params=None, **meta): + """Eradicate code checking. + + :return list: List of errors. + """ + code = converter(code) + line_numbers = commented_out_code_line_numbers(code) + lines = code.split('\n') + + result = [] + for line_number in line_numbers: + line = lines[line_number - 1] + result.append(dict( + lnum=line_number, + offset=len(line) - len(line.rstrip()), + # https://github.com/sobolevn/flake8-eradicate#output-example + text=converter('E800: Found commented out code: ') + line, + # https://github.com/sobolevn/flake8-eradicate#error-codes + type='E800', + )) + return result diff --git a/bin/pylama/lint/pylama_pycodestyle.py b/bin/pylama/lint/pylama_pycodestyle.py index cf9e245..790ceae 100644 --- a/bin/pylama/lint/pylama_pycodestyle.py +++ b/bin/pylama/lint/pylama_pycodestyle.py @@ -1,5 +1,5 @@ """pycodestyle support.""" -from pycodestyle import BaseReport, StyleGuide, get_parser +from pycodestyle import BaseReport, StyleGuide, get_parser, _parse_multi_options from pylama.lint import Linter as Abstract @@ -24,9 +24,13 @@ def run(path, code=None, params=None, **meta): for option in parser.option_list: if option.dest and option.dest in params: value = params[option.dest] - if not isinstance(value, str): - continue - params[option.dest] = option.convert_value(option, params[option.dest]) + if isinstance(value, str): + params[option.dest] = option.convert_value(option, value) + + for key in ["filename", "exclude", "select", "ignore"]: + if key in params and isinstance(params[key], str): + params[key] = _parse_multi_options(params[key]) + P8Style = StyleGuide(reporter=_PycodestyleReport, **params) buf = StringIO(code) return P8Style.input_file(path, lines=buf.readlines()) diff --git a/bin/pylama/lint/pylama_pylint/main.py b/bin/pylama/lint/pylama_pylint.py similarity index 81% rename from bin/pylama/lint/pylama_pylint/main.py rename to bin/pylama/lint/pylama_pylint.py index 3c2f1b2..31ff12f 100644 --- a/bin/pylama/lint/pylama_pylint/main.py +++ b/bin/pylama/lint/pylama_pylint.py @@ -4,6 +4,7 @@ from astroid import MANAGER from pylama.lint import Linter as BaseLinter +from pylint.__pkginfo__ import numversion from pylint.lint import Run from pylint.reporters import BaseReporter @@ -17,7 +18,6 @@ class Linter(BaseLinter): - """Check code with Pylint.""" @staticmethod @@ -54,33 +54,32 @@ def handle_message(self, msg): reporter = Reporter() - Run([path] + params.to_attrs(), reporter=reporter, exit=False) + kwargs = { + (numversion[0] == 1 and 'exit' or 'do_exit'): False + } + + Run([path] + params.to_attrs(), reporter=reporter, **kwargs) return reporter.errors class _Params(object): - """Store pylint params.""" def __init__(self, select=None, ignore=None, params=None): - params = dict(params.items()) - rcfile = params.get('rcfile', LAMA_RCFILE) - enable = params.get('enable', None) - disable = params.get('disable', None) + params = dict(params) if op.exists(HOME_RCFILE): - rcfile = HOME_RCFILE + params['rcfile'] = HOME_RCFILE if select: - enable = select | set(enable.split(",") if enable else []) + enable = params.get('enable', None) + params['enable'] = select | set(enable.split(",") if enable else []) if ignore: - disable = ignore | set(disable.split(",") if disable else []) - - params.update(dict( - rcfile=rcfile, enable=enable, disable=disable)) + disable = params.get('disable', None) + params['disable'] = ignore | set(disable.split(",") if disable else []) self.params = dict( (name.replace('_', '-'), self.prepare_value(value)) diff --git a/bin/pylama/lint/pylama_pylint/__init__.py b/bin/pylama/lint/pylama_pylint/__init__.py deleted file mode 100644 index 726c012..0000000 --- a/bin/pylama/lint/pylama_pylint/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Support pylint in Pylama.""" - -# Module information -# ================== - - -__version__ = "3.0.1" -__project__ = "pylama_pylint" -__author__ = "horneds " -__license__ = "BSD" - -from .main import Linter # noqa diff --git a/bin/pylama/lint/pylama_pylint/pylint.rc b/bin/pylama/lint/pylama_pylint/pylint.rc deleted file mode 100644 index 799c62f..0000000 --- a/bin/pylama/lint/pylama_pylint/pylint.rc +++ /dev/null @@ -1,23 +0,0 @@ -[MESSAGES CONTROL] -# Disable the message(s) with the given id(s). -# http://pylint-messages.wikidot.com/all-codes -# -# C0103: Invalid name "%s" (should match %s) -# E1101: %s %r has no %r member -# R0901: Too many ancestors (%s/%s) -# R0902: Too many instance attributes (%s/%s) -# R0903: Too few public methods (%s/%s) -# R0904: Too many public methods (%s/%s) -# R0913: Too many arguments (%s/%s) -# R0915: Too many statements (%s/%s) -# W0141: Used builtin function %r -# W0142: Used * or ** magic -# W0221: Arguments number differs from %s method -# W0232: Class has no __init__ method -# W0613: Unused argument %r -# W0631: Using possibly undefined loop variable %r -# -disable = C0103,E1101,R0901,R0902,R0903,R0904,R0913,R0915,W0141,W0142,W0221,W0232,W0613,W0631 - -[TYPECHECK] -generated-members = REQUEST,acl_users,aq_parent,objects,DoesNotExist,_meta,status_code,content,context diff --git a/bin/pylama/main.py b/bin/pylama/main.py index cfcf01c..eb3e0f1 100644 --- a/bin/pylama/main.py +++ b/bin/pylama/main.py @@ -7,7 +7,7 @@ from .config import parse_options, CURDIR, setup_logger from .core import LOGGER, run -from .async import check_async +from .check_async import check_async def check_path(options, rootdir=None, candidates=None, code=None): @@ -25,7 +25,8 @@ def check_path(options, rootdir=None, candidates=None, code=None): path = op.abspath(path_) if op.isdir(path): for root, _, files in walk(path): - candidates += [op.relpath(op.join(root, f), CURDIR) for f in files] + candidates += [op.relpath(op.join(root, f), CURDIR) + for f in files] else: candidates.append(path) @@ -35,7 +36,8 @@ def check_path(options, rootdir=None, candidates=None, code=None): paths = [] for path in candidates: - if not options.force and not any(l.allow(path) for _, l in options.linters): + if not options.force and not any(l.allow(path) + for _, l in options.linters): continue if not op.exists(path): @@ -43,7 +45,7 @@ def check_path(options, rootdir=None, candidates=None, code=None): paths.append(path) - if options.async: + if options.concurrent: return check_async(paths, options, rootdir) errors = []