From 70d8eab3bee42fbde292b4fa2813e69d9a5e45fe Mon Sep 17 00:00:00 2001 From: Jan Gosmann Date: Mon, 10 Jun 2013 13:54:52 +0200 Subject: [PATCH 001/545] Fix slow saving by not clearing cache every time. --- pylibs/rope/base/pycore.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pylibs/rope/base/pycore.py b/pylibs/rope/base/pycore.py index 32056a0f..164f669c 100644 --- a/pylibs/rope/base/pycore.py +++ b/pylibs/rope/base/pycore.py @@ -258,7 +258,6 @@ def analyze_module(self, resource, should_analyze=lambda py: True, if followed_calls is None: followed_calls = self.project.prefs.get('soa_followed_calls', 0) pymodule = self.resource_to_pyobject(resource) - self.module_cache.forget_all_data() rope.base.oi.soa.analyze_module( self, pymodule, should_analyze, search_subscopes, followed_calls) @@ -306,7 +305,7 @@ def __init__(self, pycore): def _invalidate_resource(self, resource): if resource in self.module_map: - self.forget_all_data() + self.module_map[resource]._forget_concluded_data() self.observer.remove_resource(resource) del self.module_map[resource] From 328111ebfba91826ca21fb78ccff3523dfd37853 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Sat, 15 Jun 2013 16:39:43 +0100 Subject: [PATCH 002/545] Remove leftovers of auto session support. Should have been removed on a previous commit. --- autoload/pymode/troubleshooting.vim | 1 - ftplugin/python/init-pymode.vim | 8 -------- 2 files changed, 9 deletions(-) diff --git a/autoload/pymode/troubleshooting.vim b/autoload/pymode/troubleshooting.vim index dca1ea7d..1c883a6e 100644 --- a/autoload/pymode/troubleshooting.vim +++ b/autoload/pymode/troubleshooting.vim @@ -71,7 +71,6 @@ fun! pymode#troubleshooting#Test() "{{{ call append('$', 'let pymode_rope_autocomplete_map = ' . string(g:pymode_rope_autocomplete_map)) call append('$', 'let pymode_rope_auto_project = ' . string(g:pymode_rope_auto_project)) call append('$', 'let pymode_rope_auto_project_open = ' . string(g:pymode_rope_auto_project_open)) - call append('$', 'let pymode_rope_auto_session_manage = ' . string(g:pymode_rope_auto_session_manage)) end call append('$', 'let pymode_folding = ' . string(g:pymode_folding)) call append('$', 'let pymode_breakpoint = ' . string(g:pymode_breakpoint)) diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index 59e287ea..2998e14e 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -201,9 +201,6 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope " `.ropeproject` subdirectory. call pymode#Default("g:pymode_rope_auto_project_open", 1) - " OPTION: g:pymode_rope_auto_session_manage -- bool - call pymode#Default("g:pymode_rope_auto_session_manage", 0) - " OPTION: g:pymode_rope_enable_autoimport -- bool. Enable autoimport call pymode#Default("g:pymode_rope_enable_autoimport", 1) @@ -313,11 +310,6 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope call RopeOpenExistingProject() endif - if !pymode#Default("g:pymode_rope_auto_session_manage", 0) || g:pymode_rope_auto_session_manage - autocmd VimLeave * call RopeSaveSession() - autocmd VimEnter * call RopeRestoreSession() - endif - endif " }}} From 63d92714b5f3e9f7c1985b26da9361abea083edd Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Tue, 30 Jul 2013 10:47:15 +0800 Subject: [PATCH 003/545] Update pylama --- pylibs/pylama/__init__.py | 2 +- pylibs/pylama/config.py | 174 ++++++++++++++++++++++++++++++++++++++ pylibs/pylama/core.py | 12 +-- pylibs/pylama/hook.py | 15 +++- pylibs/pylama/inirama.py | 10 --- pylibs/pylama/main.py | 163 +++++++---------------------------- pylibs/pylama/tasks.py | 24 +++--- pylibs/pymode/lint.py | 65 +++----------- 8 files changed, 247 insertions(+), 218 deletions(-) create mode 100644 pylibs/pylama/config.py diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index acbcf35b..3d7ddc52 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -5,7 +5,7 @@ :license: BSD, see LICENSE for more details. """ -version_info = 1, 3, 0 +version_info = 1, 3, 3 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/config.py b/pylibs/pylama/config.py new file mode 100644 index 00000000..78933872 --- /dev/null +++ b/pylibs/pylama/config.py @@ -0,0 +1,174 @@ +""" Parse arguments from command line and configuration files. """ +import fnmatch +import logging +from argparse import ArgumentParser, Namespace as Options +from os import getcwd, path +from re import compile as re + +from . import version, utils +from .core import DEFAULT_LINTERS, LOGGER, STREAM +from .inirama import Namespace + + +DEFAULT_COMPLEXITY = 10 +CURDIR = getcwd() +DEFAULT_INI_PATH = path.join(CURDIR, 'pylama.ini') + + +def parse_options( + args=None, async=False, select='', ignore='', linters=DEFAULT_LINTERS, + complexity=DEFAULT_COMPLEXITY, options=DEFAULT_INI_PATH): + """ Parse options from command line and configuration files. + + :return argparse.Namespace: + + """ + parser = get_parser() + actions = dict((a.dest, a) for a in parser._actions) + options = Options( + async=_Default(async), format=_Default('pep8'), + select=_Default(select), ignore=_Default(ignore), + report=_Default(None), verbose=_Default(False), + linters=_Default(linters), complexity=_Default(complexity), + options=_Default(options)) + + if not (args is None): + options = parser.parse_args(args) + + config = get_config(str(options.options)) + + for k, v in config.default.items(): + value = getattr(options, k, _Default(None)) + if not isinstance(value, _Default): + continue + + action = actions.get(k) + LOGGER.info('Find option %s (%s)', k, v) + name, value = action.dest, action.type(v)\ + if callable(action.type) else v + if action.const: + value = bool(int(value)) + setattr(options, name, value) + + opts = dict(options.__dict__.items()) + for name, value in opts.items(): + if isinstance(value, _Default): + action = actions.get(name) + if action and callable(action.type): + value.value = action.type(value.value) + + setattr(options, name, value.value) + + options.file_params = dict() + for k, s in config.sections.items(): + if k != config.default_section: + mask = re(fnmatch.translate(k)) + options.file_params[mask] = dict(s) + options.file_params[mask]['lint'] = int( + options.file_params[mask].get('lint', 1) + ) + + return options + + +def setup_logger(options): + """ Setup logger with options. """ + + LOGGER.setLevel(logging.INFO if options.verbose else logging.WARN) + if options.report: + LOGGER.removeHandler(STREAM) + LOGGER.addHandler(logging.FileHandler(options.report, mode='w')) + LOGGER.info('Try to read configuration from: ' + options.options) + + +def get_parser(): + """ Make command parser for pylama. + + :return ArgumentParser: + + """ + split_csp_str = lambda s: list( + set(i for i in s.strip().split(',') if i)) + + parser = ArgumentParser(description="Code audit tool for python.") + parser.add_argument( + "path", nargs='?', default=_Default(CURDIR), + help="Path on file or directory.") + + parser.add_argument( + "--verbose", "-v", action='store_true', help="Verbose mode.") + + parser.add_argument('--version', action='version', + version='%(prog)s ' + version) + + parser.add_argument( + "--format", "-f", default=_Default('pep8'), choices=['pep8', 'pylint'], + help="Error format.") + + parser.add_argument( + "--select", "-s", default=_Default(''), type=split_csp_str, + help="Select errors and warnings. (comma-separated)") + + parser.add_argument( + "--linters", "-l", default=_Default(','.join(DEFAULT_LINTERS)), + type=split_csp_str, + help=( + "Select linters. (comma-separated). Choices are %s." + % ','.join(s for s in utils.__all__) + )) + + parser.add_argument( + "--ignore", "-i", default=_Default(''), type=split_csp_str, + help="Ignore errors and warnings. (comma-separated)") + + parser.add_argument( + "--skip", default=_Default(''), + type=lambda s: [re(fnmatch.translate(p)) for p in s.split(',') if p], + help="Skip files by masks (comma-separated, Ex. */messages.py)") + + parser.add_argument( + "--complexity", "-c", default=_Default(DEFAULT_COMPLEXITY), type=int, + help="Set mccabe complexity.") + + parser.add_argument("--report", "-r", help="Filename for report.") + parser.add_argument( + "--hook", action="store_true", help="Install Git (Mercurial) hook.") + + parser.add_argument( + "--async", action="store_true", + help="Enable async mode. Usefull for checking a lot of files. " + "Dont supported with pylint.") + + parser.add_argument( + "--options", "-o", default=_Default(DEFAULT_INI_PATH), + help="Select configuration file. By default is '/pylama.ini'") + + return parser + + +def get_config(ini_path=DEFAULT_INI_PATH): + """ Load configuration from INI. + + :return Namespace: + + """ + config = Namespace() + config.default_section = 'main' + config.read(ini_path) + + return config + + +class _Default(object): + + def __init__(self, value): + self.value = value + + def __getattr__(self, name): + return getattr(self.value, name) + + def __str__(self): + return str(self.value) + + +# lint_ignore=R0914,W0212,E1103,C901 diff --git a/pylibs/pylama/core.py b/pylibs/pylama/core.py index 33d5e239..81620a13 100644 --- a/pylibs/pylama/core.py +++ b/pylibs/pylama/core.py @@ -1,4 +1,5 @@ -""" Pylama core functionality. Get params and runs the checkers. +""" Pylama's core functionality. Prepare params, check a modeline and run the + checkers. """ import logging import re @@ -24,12 +25,13 @@ def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, **meta): - """ Run code checking for path. + """ Run a code checkers with given params. :return errors: list of dictionaries with error's information """ errors = [] + params = dict(ignore=ignore, select=select) try: with open(path, 'rU') as f: @@ -92,7 +94,7 @@ def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, def parse_modeline(code): - """ Parse modeline params from file. + """ Parse params from file's modeline. :return dict: Linter params. @@ -105,7 +107,7 @@ def parse_modeline(code): def prepare_params(*configs, **params): - """ Prepare and merge a params from modeline or config. + """ Prepare and merge a params from modelines and configs. :return dict: @@ -129,7 +131,7 @@ def prepare_params(*configs, **params): def filter_errors(e, select=None, ignore=None, **params): - """ Filter a erros by select and ignore lists. + """ Filter a erros by select and ignore options. :return bool: diff --git a/pylibs/pylama/hook.py b/pylibs/pylama/hook.py index 0f3bddad..2a3a5b6c 100644 --- a/pylibs/pylama/hook.py +++ b/pylibs/pylama/hook.py @@ -7,6 +7,7 @@ from subprocess import Popen, PIPE from .main import LOGGER +from .config import parse_options, setup_logger try: @@ -32,8 +33,12 @@ def git_hook(): from .main import check_files _, files_modified, _ = run("git diff-index --cached --name-only HEAD") - LOGGER.setLevel('WARN') - check_files([f for f in map(str, files_modified) if f.endswith('.py')]) + + options = parse_options() + setup_logger(options) + check_files( + [f for f in map(str, files_modified) if f.endswith('.py')], options + ) def hg_hook(ui, repo, node=None, **kwargs): @@ -51,8 +56,10 @@ def hg_hook(ui, repo, node=None, **kwargs): seen.add(file_) if file_.endswith('.py'): paths.append(file_) - LOGGER.setLevel('WARN') - check_files(paths) + + options = parse_options() + setup_logger(options) + check_files(paths, options) def install_git(path): diff --git a/pylibs/pylama/inirama.py b/pylibs/pylama/inirama.py index 8e665724..44a196cb 100644 --- a/pylibs/pylama/inirama.py +++ b/pylibs/pylama/inirama.py @@ -1,16 +1,6 @@ """ Inirama is a python module that parses INI files. - .. _badges: - .. include:: ../README.rst - :start-after: .. _badges: - :end-before: .. _contents: - - .. _description: - .. include:: ../README.rst - :start-after: .. _description: - :end-before: .. _badges: - :copyright: 2013 by Kirill Klenov. :license: BSD, see LICENSE for more details. """ diff --git a/pylibs/pylama/main.py b/pylibs/pylama/main.py index 4b449746..3b0231c3 100644 --- a/pylibs/pylama/main.py +++ b/pylibs/pylama/main.py @@ -1,16 +1,12 @@ -""" Pylama shell integration. +""" Pylama's shell support. """ from __future__ import absolute_import, with_statement -import fnmatch -import logging -import re import sys -from argparse import ArgumentParser -from os import getcwd, walk, path as op +from os import walk, path as op -from . import utils, version -from .core import DEFAULT_LINTERS, LOGGER, STREAM +from .config import parse_options, CURDIR, setup_logger +from .core import LOGGER DEFAULT_COMPLEXITY = 10 @@ -19,119 +15,36 @@ def shell(args=None, error=True): """ Endpoint for console. + Parse a command arguments, configuration files and run a checkers. + :return list: list of errors :raise SystemExit: """ - curdir = getcwd() if args is None: args = sys.argv[1:] - parser = ArgumentParser(description="Code audit tool for python.") - parser.add_argument("path", nargs='?', default=curdir, - help="Path on file or directory.") - parser.add_argument( - "--verbose", "-v", action='store_true', help="Verbose mode.") - parser.add_argument('--version', action='version', - version='%(prog)s ' + version) - - split_csp_list = lambda s: list(set(i for i in s.split(',') if i)) - - parser.add_argument( - "--format", "-f", default='pep8', choices=['pep8', 'pylint'], - help="Error format.") - parser.add_argument( - "--select", "-s", default='', - type=split_csp_list, - help="Select errors and warnings. (comma-separated)") - parser.add_argument( - "--linters", "-l", default=','.join(DEFAULT_LINTERS), - type=split_csp_list, - help=( - "Select linters. (comma-separated). Choices are %s." - % ','.join(s for s in utils.__all__) - )) - parser.add_argument( - "--ignore", "-i", default='', - type=split_csp_list, - help="Ignore errors and warnings. (comma-separated)") - parser.add_argument( - "--skip", default='', - type=lambda s: [re.compile(fnmatch.translate(p)) - for p in s.split(',')], - help="Skip files by masks (comma-separated, Ex. */messages.py)") - parser.add_argument("--complexity", "-c", default=DEFAULT_COMPLEXITY, - type=int, help="Set mccabe complexity.") - parser.add_argument("--report", "-r", help="Filename for report.") - parser.add_argument("--hook", action="store_true", - help="Install Git (Mercurial) hook.") - parser.add_argument( - "--async", action="store_true", - help="Enable async mode. Usefull for checking a lot of files. " - "Dont supported with pylint.") - parser.add_argument( - "--options", "-o", default=op.join(curdir, 'pylama.ini'), - help="Select configuration file. By default is '/pylama.ini'") - options = parser.parse_args(args) - actions = dict((a.dest, a) for a in parser._actions) - - # Read options from configuration file - from .inirama import Namespace - - config = Namespace() - config.default_section = 'main' - config.read(options.options) - for k, v in config.default.items(): - action = actions.get(k) - if action: - LOGGER.info('Find option %s (%s)', k, v) - name, value = action.dest, action.type(v)\ - if callable(action.type) else v - if action.const: - value = bool(int(value)) - setattr(options, name, value) - - # Setup LOGGER - LOGGER.setLevel(logging.INFO if options.verbose else logging.WARN) - if options.report: - LOGGER.removeHandler(STREAM) - LOGGER.addHandler(logging.FileHandler(options.report, mode='w')) - LOGGER.info('Try to read configuration from: ' + options.options) + options = parse_options(args) + setup_logger(options) # Install VSC hook if options.hook: from .hook import install_hook - install_hook(options.path) - - else: - - paths = [options.path] - - if op.isdir(options.path): - paths = [] - for root, _, files in walk(options.path): - paths += [ - op.relpath(op.join(root, f), curdir) - for f in files if f.endswith('.py')] - - return check_files( - paths, - async=options.async, - rootpath=curdir, - skip=options.skip, - frmt=options.format, - ignore=options.ignore, - select=options.select, - linters=options.linters, - complexity=options.complexity, - config=config, - error=error, - ) - - -def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, - select=None, ignore=None, linters=DEFAULT_LINTERS, - complexity=DEFAULT_COMPLEXITY, config=None, error=True): + return install_hook(options.path) + + paths = [options.path] + + if op.isdir(options.path): + paths = [] + for root, _, files in walk(options.path): + paths += [ + op.relpath(op.join(root, f), CURDIR) + for f in files if f.endswith('.py')] + + return check_files(paths, options, error=error) + + +def check_files(paths, options, rootpath=None, error=True): """ Check files. :return list: list of errors @@ -140,28 +53,21 @@ def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, """ from .tasks import async_check_files - rootpath = rootpath or getcwd() + if rootpath is None: + rootpath = CURDIR + pattern = "%(rel)s:%(lnum)s:%(col)s: %(text)s" - if frmt == 'pylint': + if options.format == 'pylint': pattern = "%(rel)s:%(lnum)s: [%(type)s] %(text)s" - params = dict() - if config: - for key, section in config.sections.items(): - if key != config.default_section: - mask = re.compile(fnmatch.translate(key)) - params[mask] = prepare_params(section) - work_paths = [] for path in paths: - if skip and any(pattern.match(path) for pattern in skip): + if options.skip and any(p.match(path) for p in options.skip): LOGGER.info('Skip path: %s', path) continue work_paths.append(path) - errors = async_check_files( - work_paths, async=async, rootpath=rootpath, ignore=ignore, - select=select, linters=linters, complexity=complexity, params=params) + errors = async_check_files(work_paths, options, rootpath=rootpath) for er in errors: LOGGER.warning(pattern, er) @@ -172,16 +78,5 @@ def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, return errors -def prepare_params(section): - """ Parse modeline params from configuration. - - :return dict: Linter params. - - """ - params = dict(section) - params['lint'] = int(params.get('lint', 1)) - return params - - if __name__ == '__main__': shell() diff --git a/pylibs/pylama/tasks.py b/pylibs/pylama/tasks.py index 485b8905..250b07ee 100644 --- a/pylibs/pylama/tasks.py +++ b/pylibs/pylama/tasks.py @@ -40,7 +40,7 @@ def run(self): self.path_queue.task_done() -def async_check_files(paths, async=False, linters=None, **params): +def async_check_files(paths, options, rootpath=None): """ Check paths. :return list: list of errors @@ -50,12 +50,11 @@ def async_check_files(paths, async=False, linters=None, **params): errors = [] # Disable async if pylint enabled - async = async and not 'pylint' in linters - params['linters'] = linters + async = options.async and not 'pylint' in options.linters if not async: for path in paths: - errors += check_path(path, **params) + errors += check_path(path, options=options, rootpath=rootpath) return errors LOGGER.info('Async code checking is enabled.') @@ -68,7 +67,7 @@ def async_check_files(paths, async=False, linters=None, **params): worker.start() for path in paths: - path_queue.put((path, params)) + path_queue.put((path, dict(options=options, rootpath=rootpath))) path_queue.join() @@ -81,8 +80,7 @@ def async_check_files(paths, async=False, linters=None, **params): return errors -def check_path(path, rootpath='.', ignore=None, select=None, linters=None, - complexity=None, params=None): +def check_path(path, options=None, rootpath=None, **meta): """ Check path. :return list: list of errors @@ -90,17 +88,19 @@ def check_path(path, rootpath='.', ignore=None, select=None, linters=None, """ LOGGER.info("Parse file: %s", path) - params = params or dict() config = dict() + if rootpath is None: + rootpath = '.' - for mask in params: + for mask in options.file_params: if mask.match(path): - config.update(params[mask]) + config.update(options.file_params[mask]) errors = [] for error in run( - path, ignore=ignore, select=select, linters=linters, - complexity=complexity, config=config): + path, ignore=options.ignore, select=options.select, + linters=options.linters, complexity=options.complexity, + config=config, **meta): try: error['rel'] = op.relpath(error['filename'], rootpath) error['col'] = error.get('col', 1) diff --git a/pylibs/pymode/lint.py b/pylibs/pymode/lint.py index 96a7134a..3f19dfa5 100644 --- a/pylibs/pymode/lint.py +++ b/pylibs/pymode/lint.py @@ -1,15 +1,11 @@ """ Pylama support. """ from __future__ import absolute_import -import fnmatch import json import locale -from os import path as op -from re import compile as re -from pylama.core import run -from pylama.inirama import Namespace -from pylama.main import prepare_params +from pylama.main import parse_options +from pylama.tasks import check_path from . import interface from .queue import add_task @@ -23,56 +19,23 @@ def check_file(): """ Check current buffer. """ - checkers = interface.get_option('lint_checker').split(',') buf = interface.get_current_buffer() - # Check configuration from `pymode.ini` - curdir = interface.eval_code('getcwd()') - config = Namespace() - config.default_section = 'main' - config.read(op.join(curdir, 'pylama.ini'), op.join(curdir, 'pymode.ini')) - - ignore = set([ - i for i in ( - interface.get_option('lint_ignore').split(',') + - interface.get_var('lint_ignore').split(',') + - config.default.get('ignore', '').split(',') - ) if i - ]) - - select = set([ - s for s in ( - interface.get_option('lint_select').split(',') + - interface.get_var('lint_select').split(',') + - config.default.get('select', '').split(',') - ) if s - ]) - - complexity = int(interface.get_option('lint_mccabe_complexity') or 0) - - params = dict() - relpath = op.relpath(buf.name, curdir) - for mask in config.sections: - mask_re = re(fnmatch.translate(mask)) - if mask_re.match(relpath): - params.update(prepare_params(config[mask])) - - if relpath in config: - params = prepare_params(config[relpath]) + linters = interface.get_option('lint_checker') + ignore = interface.get_option('lint_ignore') + select = interface.get_option('lint_select') + complexity = interface.get_option('lint_mccabe_complexity') or '0' - add_task( - run_checkers, - callback=parse_result, - title='Code checking', + options = parse_options( + ignore=ignore, select=select, complexity=complexity, linters=linters) - # params - checkers=checkers, ignore=ignore, buf=buf, select=select, - complexity=complexity, config=params, + add_task( + run_checkers, callback=parse_result, title='Code checking', buf=buf, + options=options, ) -def run_checkers(checkers=None, ignore=None, buf=None, select=None, - complexity=None, callback=None, config=None): +def run_checkers(callback=None, buf=None, options=None): """ Run pylama code. :return list: errors @@ -81,9 +44,7 @@ def run_checkers(checkers=None, ignore=None, buf=None, select=None, pylint_options = '--rcfile={0} -r n'.format( interface.get_var('lint_config')).split() - return run( - buf.name, ignore=ignore, select=select, linters=checkers, - pylint=pylint_options, complexity=complexity, config=config) + return check_path(buf.name, options=options, pylint=pylint_options) def parse_result(result, buf=None, **kwargs): From dce446a6419061ddd2b715b3ba851b0c32ce6999 Mon Sep 17 00:00:00 2001 From: Lawrence Jacob Siebert Date: Sun, 4 Aug 2013 18:49:26 -0700 Subject: [PATCH 004/545] Update README.rst better english finded is not a word, and be is not past tense. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cf930d06..70b969d6 100644 --- a/README.rst +++ b/README.rst @@ -165,7 +165,7 @@ Default values: :: " Check code every save let g:pymode_lint_write = 1 - " Auto open cwindow if errors be finded + " Auto open cwindow if errors were found let g:pymode_lint_cwindow = 1 " Show error message if cursor placed at the error line From 5f2a0f78f2d9ecb1c0d12a1c7465caed167db0b0 Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Wed, 7 Aug 2013 06:39:13 +1000 Subject: [PATCH 005/545] Don't use a t.co link for the screencast link. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cf930d06..e32bdb62 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ There is no need to install the pylint_, rope_ or any used python library on you - Powerful customization - And more, more ... -See (very old) screencast here: http://t.co/3b0bzeXA (sorry for quality, this is my first screencast) +See (very old) screencast here: http://www.youtube.com/watch?v=67OZNp9Z0CQ (sorry for quality, this is my first screencast) Another old presentation here: http://www.youtube.com/watch?v=YhqsjUUHj6g From 6e2f135c390b4fc0c2e8804c367ee32196f16aa4 Mon Sep 17 00:00:00 2001 From: Lawrence Akka Date: Sat, 17 Aug 2013 23:43:58 +0200 Subject: [PATCH 006/545] Allow Pyrun to deal with sys.exit termination --- autoload/pymode/run.vim | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index df971178..2d07d9b3 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -17,7 +17,18 @@ fun! pymode#run#Run(line1, line2) "{{{ try py context = globals() py context['raw_input'] = context['input'] = lambda s: vim.eval('input("{0}")'.format(s)) - py execfile(vim.eval('expand("%:p")'), context) +python << ENDPYTHON +try: + execfile(vim.eval('expand("%:p")'), context) +# Vim cannot handle a SystemExit error raised by Python, so we need to trap it here, and +# handle it specially +except SystemExit as e: + if e.code: + # A non-false code indicates abnormal termination. A false code will be treated as a + # successful run, and the error will be hidden from Vim + vim.command('echohl Error | echo "Script exited with code {0}" | echohl none'.format(e.code)) + vim.command('return') +ENDPYTHON py out, err = sys.stdout.getvalue().strip(), sys.stderr.getvalue() py sys.stdout, sys.stderr = stdout_, stderr_ @@ -38,7 +49,7 @@ else: EOF catch /.*/ - + echohl Error | echo "Run-time error." | echohl none endtry From 1d0969e9e5e1c7b0e0cfb70f1ea84bdddb32dfc3 Mon Sep 17 00:00:00 2001 From: Lawrence Akka Date: Mon, 19 Aug 2013 23:06:22 +0200 Subject: [PATCH 007/545] Fixing the quickfix window output. Better errorformat --- autoload/pymode/run.vim | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index 2d07d9b3..cc72c8a7 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -31,11 +31,23 @@ except SystemExit as e: ENDPYTHON py out, err = sys.stdout.getvalue().strip(), sys.stderr.getvalue() py sys.stdout, sys.stderr = stdout_, stderr_ - cexpr "" - py for x in err.strip().split('\n'): vim.command('caddexpr "' + x.replace('"', r'\"') + '"') + let l:traceback = [] let l:oldefm = &efm - set efm=%C\ %.%#,%A\ \ File\ \"%f\"\\,\ line\ %l%.%#,%Z%[%^\ ]%\\@=%m + let &efm = '%E File "%f"\, line %l\,%m,' + let &efm .= '%E File "%f"\, line %l,' + let &efm .= '%C%p^,' + let &efm .= '%+C %.%#,' + let &efm .= '%+C %.%#,' + let &efm .= '%Z%m,' + let &efm .= '%-G%.%#' + +python << ENDPYTHON2 +for x in [i for i in err.splitlines() if "" not in i]: + vim.command("call add(l:traceback, '{}')".format(x)) +ENDPYTHON2 + + cgetexpr(l:traceback) call pymode#QuickfixOpen(0, g:pymode_lint_hold, g:pymode_lint_maxheight, g:pymode_lint_minheight, 0) let &efm = l:oldefm @@ -49,8 +61,8 @@ else: EOF catch /.*/ - + echohl Error | echo "Run-time error." | echohl none - + endtry endfunction "}}} From e1e63cc72130d03bdce991eadb9bf84dfe9b3ae9 Mon Sep 17 00:00:00 2001 From: Lawrence Akka Date: Wed, 21 Aug 2013 17:07:04 +0100 Subject: [PATCH 008/545] Adding 'Traceback' line to quickfix errors, for clarity --- autoload/pymode/run.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index cc72c8a7..8af5a8d3 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -34,7 +34,8 @@ ENDPYTHON cexpr "" let l:traceback = [] let l:oldefm = &efm - let &efm = '%E File "%f"\, line %l\,%m,' + let &efm = '%+GTraceback%.%#,' + let &efm .= '%E File "%f"\, line %l\,%m,' let &efm .= '%E File "%f"\, line %l,' let &efm .= '%C%p^,' let &efm .= '%+C %.%#,' From a064c3c2983e7a6a07d513628e100b60726563a8 Mon Sep 17 00:00:00 2001 From: Lawrence Akka Date: Wed, 21 Aug 2013 17:37:44 +0100 Subject: [PATCH 009/545] Commenting the errorformat string --- autoload/pymode/run.vim | 46 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index 8af5a8d3..cbb3b81f 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -34,20 +34,58 @@ ENDPYTHON cexpr "" let l:traceback = [] let l:oldefm = &efm + + " The following lines set Vim's errorformat variable, to allow the + " quickfix window to show Python tracebacks properly. It is much + " easier to use let than set, because set requires many more + " characters to be escaped. This is much easier to read and + " maintain. % escapes are still needed however before any regex meta + " characters. Hence \S (non-whitespace) becomes %\S etc. Note that + " * becomes %#, so .* (match any character) becomes %.%# Commas must + " also be escaped, with a backslash (\,). See the Vim help on + " quickfix for details. + " + " Python errors are multi-lined. They often start with 'Traceback', so + " we want to capture that (with +G) and show it in the quickfix window + " because it explains the order of error messages. let &efm = '%+GTraceback%.%#,' - let &efm .= '%E File "%f"\, line %l\,%m,' - let &efm .= '%E File "%f"\, line %l,' + + " The error message itself starts with a line with 'File' in it. There + " are a couple of variations, and we need to process a line beginning + " with whitespace followed by File, the filename in "", a line number, + " and optional further text. %E here indicates the start of a multi-line + " error message. The %\C at the end means that a case-sensitive search is + " required. + let &efm .= '%E File "%f"\, line %l\,%m%\C,' + let &efm .= '%E File "%f"\, line %l%\C,' + + " The possible continutation lines are idenitifed to Vim by %C. We deal + " with these in order of most to least specific to ensure a proper + " match. A pointer (^) identifies the column in which the error occurs + " (but will not be entirely accurate due to indention of Python code). let &efm .= '%C%p^,' + " Any text, indented by more than two spaces contain useful information. + " We want this to appear in the quickfix window, hence %+. let &efm .= '%+C %.%#,' let &efm .= '%+C %.%#,' - let &efm .= '%Z%m,' + + " The last line (%Z) does not begin with any whitespace. We use a zero + " width lookahead (\&) to check this. The line contains the error + " message itself (%m) + let &efm .= '%Z%\S%\&%m,' + + " We can ignore any other lines (%-G) let &efm .= '%-G%.%#' python << ENDPYTHON2 +# Remove any error lines containing ''. We don't need them. +# Add the rest to a Vim list. for x in [i for i in err.splitlines() if "" not in i]: vim.command("call add(l:traceback, '{}')".format(x)) ENDPYTHON2 - +" Now we can add the list of errors to the quickfix window, and show it. We have +" to add them all at once in this way, because the errors are multi-lined and +" they won't be parsed properly otherwise. cgetexpr(l:traceback) call pymode#QuickfixOpen(0, g:pymode_lint_hold, g:pymode_lint_maxheight, g:pymode_lint_minheight, 0) let &efm = l:oldefm From 57aaea51bfbc5ecdb4297fb14bc546dd415efbdc Mon Sep 17 00:00:00 2001 From: Arun Mahapatra Date: Sat, 24 Aug 2013 15:29:54 +0530 Subject: [PATCH 010/545] Add pymode#Execute function to wrap calls to python interpreter. Extract setting up sys.path responsibility into a separate autoload module. Modify run.vim to support pymode#Execute. --- autoload/pymode.vim | 7 +++++++ autoload/pymode/path.vim | 12 ++++++++++++ autoload/pymode/run.vim | 23 +++++++++++++---------- ftplugin/python/init-pymode.vim | 20 ++++++-------------- ftplugin/python/pymode.vim | 2 +- 5 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 autoload/pymode/path.vim diff --git a/autoload/pymode.vim b/autoload/pymode.vim index c69e6798..89f19a21 100644 --- a/autoload/pymode.vim +++ b/autoload/pymode.vim @@ -12,6 +12,13 @@ fun! pymode#Default(name, default) "{{{ endfunction "}}} +fun! pymode#Execute(expression) "{{{ + " DESC: Execute an expression in the default python interpreter + " + execute 'python '.a:expression +endfunction "}}} + + fun! pymode#Option(name) "{{{ let name = 'b:pymode_' . a:name diff --git a/autoload/pymode/path.vim b/autoload/pymode/path.vim new file mode 100644 index 00000000..b692dbe1 --- /dev/null +++ b/autoload/pymode/path.vim @@ -0,0 +1,12 @@ +fun! pymode#path#Activate(plugin_root) "{{{ + +python << EOF +import sys, vim, os + +curpath = vim.eval("getcwd()") +libpath = os.path.join(vim.eval("a:plugin_root"), 'pylibs') + +sys.path = [libpath, curpath] + vim.eval("g:pymode_paths") + sys.path +EOF + +endfunction "}}} diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index df971178..5933f372 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -8,21 +8,24 @@ fun! pymode#run#Run(line1, line2) "{{{ return 0 endtry endif - py import StringIO - py sys.stdout, stdout_ = StringIO.StringIO(), sys.stdout - py sys.stderr, stderr_ = StringIO.StringIO(), sys.stderr - py enc = vim.eval('&enc') + call pymode#Execute("import StringIO") + call pymode#Execute("sys.stdout, stdout_ = StringIO.StringIO(), sys.stdout") + call pymode#Execute("sys.stderr, stderr_ = StringIO.StringIO(), sys.stderr") + call pymode#Execute("enc = vim.eval('&enc')") call setqflist([]) call pymode#WideMessage("Code running.") try - py context = globals() - py context['raw_input'] = context['input'] = lambda s: vim.eval('input("{0}")'.format(s)) - py execfile(vim.eval('expand("%:p")'), context) - py out, err = sys.stdout.getvalue().strip(), sys.stderr.getvalue() - py sys.stdout, sys.stderr = stdout_, stderr_ + call pymode#Execute("context = globals()") + call pymode#Execute("context['raw_input'] = context['input'] = lambda s: vim.eval('input(\"{0}\")'.format(s))") + call pymode#Execute("execfile(vim.eval('expand(\"%:p\")'), context)") + call pymode#Execute("out, err = sys.stdout.getvalue().strip(), sys.stderr.getvalue()") + call pymode#Execute("sys.stdout, sys.stderr = stdout_, stderr_") cexpr "" - py for x in err.strip().split('\n'): vim.command('caddexpr "' + x.replace('"', r'\"') + '"') +python << EOF +for x in err.strip().split('\n'): + vim.command('caddexpr "' + x.replace('"', r'\"') + '"') +EOF let l:oldefm = &efm set efm=%C\ %.%#,%A\ \ File\ \"%f\"\\,\ line\ %l%.%#,%Z%[%^\ ]%\\@=%m call pymode#QuickfixOpen(0, g:pymode_lint_hold, g:pymode_lint_maxheight, g:pymode_lint_minheight, 0) diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index 96ecd74a..506028cb 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -42,15 +42,7 @@ endif if !pymode#Default('g:pymode_path', 1) || g:pymode_path call pymode#Default('g:pymode_paths', []) - -python << EOF -import sys, vim, os - -curpath = vim.eval("getcwd()") -libpath = os.path.join(vim.eval("expand(':p:h:h:h')"), 'pylibs') - -sys.path = [libpath, curpath] + vim.eval("g:pymode_paths") + sys.path -EOF + call pymode#path#Activate(expand(":p:h:h:h")) endif " }}} @@ -130,9 +122,9 @@ if !pymode#Default("g:pymode_lint", 1) || g:pymode_lint let g:pymode_lint_config = expand(":p:h:h:h") . "/pylint.ini" endif - py from pymode import queue + call pymode#Execute("from pymode import queue") - au VimLeavePre * py queue.stop_queue() + au VimLeavePre * call pymode#Execute("queue.stop_queue()") endif @@ -252,7 +244,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope call pymode#Default("g:pymode_rope_always_show_complete_menu", 0) " DESC: Init Rope - py import ropevim + call pymode#Execute("import ropevim") fun! RopeCodeAssistInsertMode() "{{{ call RopeCodeAssist() @@ -263,7 +255,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope if isdirectory(getcwd() . '/.ropeproject') " In order to pass it the quiet kwarg I need to open the project " using python and not vim, which should be no major issue - py ropevim._interface.open_project(quiet=True) + call pymode#Execute("ropevim._interface.open_project(quiet=True)") return "" endif endfunction "}}} @@ -275,7 +267,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope fun! RopeOmni(findstart, base) "{{{ if a:findstart - py ropevim._interface._find_start() + call pymode#Execute("ropevim._interface._find_start()") return g:pymode_offset else call RopeOmniComplete() diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index 9b85421e..1560e2e9 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -76,7 +76,7 @@ if pymode#Option('lint') " DESC: Run queue let &l:updatetime = g:pymode_updatetime au CursorHold call pymode#queue#Poll() - au BufLeave py queue.stop_queue() + au BufLeave call pymode#Execute("queue.stop_queue()") endif From 225508defe29a55b24892641c1cf2717c1afdf68 Mon Sep 17 00:00:00 2001 From: Arun Mahapatra Date: Sat, 24 Aug 2013 15:58:16 +0530 Subject: [PATCH 011/545] Add pymode#Execute to doc, lint and queue. --- autoload/pymode/doc.vim | 10 +++++----- autoload/pymode/lint.vim | 8 ++++---- autoload/pymode/queue.vim | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/autoload/pymode/doc.vim b/autoload/pymode/doc.vim index cf997ad8..27e5b5eb 100644 --- a/autoload/pymode/doc.vim +++ b/autoload/pymode/doc.vim @@ -5,12 +5,12 @@ fun! pymode#doc#Show(word) "{{{ if a:word == '' echoerr "No name/symbol under cursor!" else - py import StringIO - py sys.stdout, _ = StringIO.StringIO(), sys.stdout - py help(vim.eval('a:word')) - py sys.stdout, out = _, sys.stdout.getvalue() + call pymode#Execute("import StringIO") + call pymode#Execute("sys.stdout, _ = StringIO.StringIO(), sys.stdout") + call pymode#Execute("help('".a:word."')") + call pymode#Execute("sys.stdout, out = _, sys.stdout.getvalue()") call pymode#TempBuffer() - py vim.current.buffer.append(str(out).split('\n'), 0) + call pymode#Execute("vim.current.buffer.append(str(out).splitlines(), 0)") wincmd p endif endfunction "}}} diff --git a/autoload/pymode/lint.vim b/autoload/pymode/lint.vim index 68782e18..37e9d4fa 100644 --- a/autoload/pymode/lint.vim +++ b/autoload/pymode/lint.vim @@ -14,8 +14,8 @@ fun! pymode#lint#Check() "{{{ let g:pymode_lint_buffer = bufnr('%') - py from pymode import lint - py lint.check_file() + call pymode#Execute("from pymode import lint") + call pymode#Execute("lint.check_file()") endfunction " }}} @@ -100,8 +100,8 @@ fun! pymode#lint#Auto() "{{{ return 0 endtry endif - py from pymode import auto - py auto.fix_current_file() + call pymode#Execute("from pymode import auto") + call pymode#Execute("auto.fix_current_file()") cclose edit call pymode#WideMessage("AutoPep8 done.") diff --git a/autoload/pymode/queue.vim b/autoload/pymode/queue.vim index b3160ee0..656049a7 100644 --- a/autoload/pymode/queue.vim +++ b/autoload/pymode/queue.vim @@ -1,7 +1,8 @@ fun! pymode#queue#Poll() "{{{ " Check current tasks - py queue.check_task() + call pymode#Execute("from pymode import queue") + call pymode#Execute("queue.check_task()") " Update interval if mode() == 'i' From b56321512a76b2d273bf04a7c7ef1cc9fdb5bd09 Mon Sep 17 00:00:00 2001 From: Arun Mahapatra Date: Sun, 25 Aug 2013 09:54:21 +0530 Subject: [PATCH 012/545] Do not indent and other ftplugin if g:pymode is zero. --- after/indent/python.vim | 4 ++++ ftplugin/python/pymode.vim | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/after/indent/python.vim b/after/indent/python.vim index 1e324f23..969488c3 100644 --- a/after/indent/python.vim +++ b/after/indent/python.vim @@ -1,3 +1,7 @@ +if pymode#Default('g:pymode', 1) || !g:pymode + finish +endif + if pymode#Default('b:pymode_indent', 1) || !g:pymode_indent finish endif diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index 9b85421e..c4722219 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -1,5 +1,9 @@ runtime ftplugin/python/init-pymode.vim +if pymode#Default('g:pymode', 1) || !g:pymode + finish +endif + if pymode#Default('b:pymode', 1) finish endif From 6b94dc20761bd9430dd52e2818b810af213b5530 Mon Sep 17 00:00:00 2001 From: Arun Mahapatra Date: Sun, 25 Aug 2013 10:00:11 +0530 Subject: [PATCH 013/545] Remove unnecessary check for pymode state in buffer scope. --- ftplugin/python/pymode.vim | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index c4722219..de9bc680 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -4,10 +4,6 @@ if pymode#Default('g:pymode', 1) || !g:pymode finish endif -if pymode#Default('b:pymode', 1) - finish -endif - " Parse pymode modeline call pymode#Modeline() From 7f9e3ad7098312a964de85d5e946f651c75eb20c Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 29 Aug 2013 18:46:18 +0700 Subject: [PATCH 014/545] Update changelog. --- Changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.rst b/Changelog.rst index 5536794b..8a166c93 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -5,6 +5,7 @@ Changelog * Removed `g:pymode_rope_map_space` option; * Added PEP257 checker; * Support 'pudb' in breakpoints; +* Pyrun can now operate on a range of lines, and does not need to save (c) lawrenceakka ## 2013-05-15 0.6.18 -------------------- From aa9d049b4492f793217b4ab107b4bf7ad29c3edc Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 29 Aug 2013 18:46:36 +0700 Subject: [PATCH 015/545] Prepare to python3 support --- AUTHORS | 1 + autoload/pymode.vim | 7 ------- autoload/pymode/breakpoint.vim | 3 ++- autoload/pymode/doc.vim | 9 ++++----- autoload/pymode/lint.vim | 8 ++++---- autoload/pymode/path.vim | 29 +++++++++++++++++------------ autoload/pymode/queue.vim | 4 ++-- autoload/pymode/virtualenv.vim | 2 +- ftplugin/python/init-pymode.vim | 19 ++++++++++++------- ftplugin/python/pymode.vim | 4 ++-- 10 files changed, 45 insertions(+), 41 deletions(-) diff --git a/AUTHORS b/AUTHORS index e0bc5bbb..797fb34e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,6 +11,7 @@ Contributors: * Denis Kasak (dkasak); * Steve Losh (sjl); * nixon; +* lawrenceakka; * tramchamploo; * Benjamin Ruston (bruston); * Robert David Grant (bgrant); diff --git a/autoload/pymode.vim b/autoload/pymode.vim index 89f19a21..c69e6798 100644 --- a/autoload/pymode.vim +++ b/autoload/pymode.vim @@ -12,13 +12,6 @@ fun! pymode#Default(name, default) "{{{ endfunction "}}} -fun! pymode#Execute(expression) "{{{ - " DESC: Execute an expression in the default python interpreter - " - execute 'python '.a:expression -endfunction "}}} - - fun! pymode#Option(name) "{{{ let name = 'b:pymode_' . a:name diff --git a/autoload/pymode/breakpoint.vim b/autoload/pymode/breakpoint.vim index b71bc596..82dd8e82 100644 --- a/autoload/pymode/breakpoint.vim +++ b/autoload/pymode/breakpoint.vim @@ -1,3 +1,4 @@ +" Quick set or delete a breakpoints fun! pymode#breakpoint#Set(lnum) "{{{ let line = getline(a:lnum) if strridx(line, g:pymode_breakpoint_cmd) != -1 @@ -8,7 +9,7 @@ fun! pymode#breakpoint#Set(lnum) "{{{ normal k endif - " Save file + " Save file without any events if &modifiable && &modified | noautocmd write | endif endfunction "}}} diff --git a/autoload/pymode/doc.vim b/autoload/pymode/doc.vim index 27e5b5eb..f5800f99 100644 --- a/autoload/pymode/doc.vim +++ b/autoload/pymode/doc.vim @@ -5,12 +5,11 @@ fun! pymode#doc#Show(word) "{{{ if a:word == '' echoerr "No name/symbol under cursor!" else - call pymode#Execute("import StringIO") - call pymode#Execute("sys.stdout, _ = StringIO.StringIO(), sys.stdout") - call pymode#Execute("help('".a:word."')") - call pymode#Execute("sys.stdout, out = _, sys.stdout.getvalue()") + Python import StringIO + Python sys.stdout, _ = StringIO.StringIO(), sys.stdout + Python help(vim.eval('a:word')) call pymode#TempBuffer() - call pymode#Execute("vim.current.buffer.append(str(out).splitlines(), 0)") + Python vim.current.buffer.append(str(out).splitlines(), 0) wincmd p endif endfunction "}}} diff --git a/autoload/pymode/lint.vim b/autoload/pymode/lint.vim index 37e9d4fa..7e6bc9b2 100644 --- a/autoload/pymode/lint.vim +++ b/autoload/pymode/lint.vim @@ -14,8 +14,8 @@ fun! pymode#lint#Check() "{{{ let g:pymode_lint_buffer = bufnr('%') - call pymode#Execute("from pymode import lint") - call pymode#Execute("lint.check_file()") + Python from pymode import lint + Python lint.check_file() endfunction " }}} @@ -100,8 +100,8 @@ fun! pymode#lint#Auto() "{{{ return 0 endtry endif - call pymode#Execute("from pymode import auto") - call pymode#Execute("auto.fix_current_file()") + Python from pymode import auto + Python auto.fix_current_file() cclose edit call pymode#WideMessage("AutoPep8 done.") diff --git a/autoload/pymode/path.vim b/autoload/pymode/path.vim index b692dbe1..e765ac7d 100644 --- a/autoload/pymode/path.vim +++ b/autoload/pymode/path.vim @@ -1,12 +1,17 @@ -fun! pymode#path#Activate(plugin_root) "{{{ - -python << EOF -import sys, vim, os - -curpath = vim.eval("getcwd()") -libpath = os.path.join(vim.eval("a:plugin_root"), 'pylibs') - -sys.path = [libpath, curpath] + vim.eval("g:pymode_paths") + sys.path -EOF - -endfunction "}}} +fun! pymode#path#Activate(plugin_root) "{{{ + +Python << EOF +import sys, vim, os + +pymode_lib = 'pylibs' + +if sys.version >= (3, 0, 0): + pymode_lib = 'pylibs3' + +curpath = vim.eval("getcwd()") +libpath = os.path.join(vim.eval("a:plugin_root"), pymode_lib) + +sys.path = [libpath, curpath] + vim.eval("g:pymode_paths") + sys.path +EOF + +endfunction "}}} diff --git a/autoload/pymode/queue.vim b/autoload/pymode/queue.vim index 656049a7..d38208b7 100644 --- a/autoload/pymode/queue.vim +++ b/autoload/pymode/queue.vim @@ -1,8 +1,8 @@ fun! pymode#queue#Poll() "{{{ " Check current tasks - call pymode#Execute("from pymode import queue") - call pymode#Execute("queue.check_task()") + Python from pymode import queue + Python queue.check_task() " Update interval if mode() == 'i' diff --git a/autoload/pymode/virtualenv.vim b/autoload/pymode/virtualenv.vim index c771d907..04267343 100644 --- a/autoload/pymode/virtualenv.vim +++ b/autoload/pymode/virtualenv.vim @@ -12,7 +12,7 @@ fun! pymode#virtualenv#Activate() "{{{ call add(g:pymode_virtualenv_enabled, $VIRTUAL_ENV) -python << EOF +Python << EOF import sys, vim, os ve_dir = vim.eval('$VIRTUAL_ENV') diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index 506028cb..a14082e2 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -13,7 +13,7 @@ if pymode#Default('g:pymode', 1) || !g:pymode endif " DESC: Check python support -if !has('python') +if !has('python') && !has('python3') let g:pymode_virtualenv = 0 let g:pymode_path = 0 let g:pymode_lint = 0 @@ -23,6 +23,11 @@ if !has('python') let g:pymode_run = 0 endif +if has('python') + command! -nargs=1 Python python +elseif has('python3') + command! -nargs=1 Python python3 +end " Virtualenv {{{ @@ -122,9 +127,9 @@ if !pymode#Default("g:pymode_lint", 1) || g:pymode_lint let g:pymode_lint_config = expand(":p:h:h:h") . "/pylint.ini" endif - call pymode#Execute("from pymode import queue") + Python from pymode import queue - au VimLeavePre * call pymode#Execute("queue.stop_queue()") + au VimLeavePre * Python queue.stop_queue() endif @@ -149,7 +154,7 @@ if !pymode#Default("g:pymode_breakpoint", 1) || g:pymode_breakpoint if !pymode#Default("g:pymode_breakpoint_cmd", "import pdb; pdb.set_trace() # XXX BREAKPOINT") && has("python") -python << EOF +Python << EOF from imp import find_module for module in ('pudb', 'ipdb'): @@ -244,7 +249,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope call pymode#Default("g:pymode_rope_always_show_complete_menu", 0) " DESC: Init Rope - call pymode#Execute("import ropevim") + Python import ropevim fun! RopeCodeAssistInsertMode() "{{{ call RopeCodeAssist() @@ -255,7 +260,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope if isdirectory(getcwd() . '/.ropeproject') " In order to pass it the quiet kwarg I need to open the project " using python and not vim, which should be no major issue - call pymode#Execute("ropevim._interface.open_project(quiet=True)") + Python ropevim._interface.open_project(quiet=True) return "" endif endfunction "}}} @@ -267,7 +272,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope fun! RopeOmni(findstart, base) "{{{ if a:findstart - call pymode#Execute("ropevim._interface._find_start()") + Python ropevim._interface._find_start() return g:pymode_offset else call RopeOmniComplete() diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index 25a839b3..d9b7a755 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -1,6 +1,6 @@ runtime ftplugin/python/init-pymode.vim -if pymode#Default('g:pymode', 1) || !g:pymode +if !g:pymode finish endif @@ -76,7 +76,7 @@ if pymode#Option('lint') " DESC: Run queue let &l:updatetime = g:pymode_updatetime au CursorHold call pymode#queue#Poll() - au BufLeave call pymode#Execute("queue.stop_queue()") + au BufLeave Python queue.stop_queue() endif From f32a5124d03983754ff7f7c46b36460890ab395a Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 29 Aug 2013 19:00:47 +0700 Subject: [PATCH 016/545] Change paths. --- autoload/pymode/path.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autoload/pymode/path.vim b/autoload/pymode/path.vim index e765ac7d..a2793e4e 100644 --- a/autoload/pymode/path.vim +++ b/autoload/pymode/path.vim @@ -5,8 +5,8 @@ import sys, vim, os pymode_lib = 'pylibs' -if sys.version >= (3, 0, 0): - pymode_lib = 'pylibs3' +# if sys.version >= (3, 0, 0): +# pymode_lib = 'pylibs3' curpath = vim.eval("getcwd()") libpath = os.path.join(vim.eval("a:plugin_root"), pymode_lib) From b1beaae3fc63c117ef533cd80e3092aba16c231e Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 29 Aug 2013 23:34:03 +0700 Subject: [PATCH 017/545] Update pylama to version 1.5.0 --- Changelog.rst | 1 + pylibs/pylama/__init__.py | 10 +- pylibs/pylama/checkers/mccabe.py | 30 +- pylibs/pylama/checkers/pep257.py | 43 ++- pylibs/pylama/checkers/pep8.py | 5 +- pylibs/pylama/checkers/pylint/__init__.py | 5 +- pylibs/pylama/checkers/pylint/__pkginfo__.py | 5 +- .../checkers/pylint/astroid/__init__.py | 118 ++++++ .../{logilab/astng => astroid}/__pkginfo__.py | 22 +- .../{logilab/astng => astroid}/as_string.py | 142 +++---- .../{logilab/astng => astroid}/bases.py | 51 ++- .../{logilab/astng => astroid}/builder.py | 54 ++- .../{logilab/astng => astroid}/exceptions.py | 29 +- .../{logilab/astng => astroid}/inference.py | 70 ++-- .../{logilab/astng => astroid}/manager.py | 153 ++++---- .../{logilab/astng => astroid}/mixins.py | 12 +- .../astng => astroid}/node_classes.py | 129 ++++--- .../{logilab/astng => astroid}/nodes.py | 10 +- .../{logilab/astng => astroid}/protocols.py | 17 +- .../astng => astroid}/raw_building.py | 60 +-- .../{logilab/astng => astroid}/rebuilder.py | 125 ++++-- .../astng => astroid}/scoped_nodes.py | 107 ++--- .../{logilab/astng => astroid}/utils.py | 30 +- .../checkers/pylint/checkers/__init__.py | 57 ++- .../pylama/checkers/pylint/checkers/base.py | 365 +++++++++++------- .../checkers/pylint/checkers/classes.py | 72 ++-- .../pylint/checkers/design_analysis.py | 94 ++--- .../checkers/pylint/checkers/exceptions.py | 57 +-- .../pylama/checkers/pylint/checkers/format.py | 72 ++-- .../checkers/pylint/checkers/imports.py | 29 +- .../checkers/pylint/checkers/logging.py | 29 +- .../pylama/checkers/pylint/checkers/misc.py | 58 +-- .../checkers/pylint/checkers/newstyle.py | 55 ++- .../checkers/pylint/checkers/raw_metrics.py | 14 +- .../checkers/pylint/checkers/similar.py | 34 +- .../pylama/checkers/pylint/checkers/stdlib.py | 68 ++++ .../checkers/pylint/checkers/strings.py | 41 +- .../checkers/pylint/checkers/typecheck.py | 84 ++-- .../pylama/checkers/pylint/checkers/utils.py | 111 ++++-- .../checkers/pylint/checkers/variables.py | 101 +++-- pylibs/pylama/checkers/pylint/config.py | 16 +- pylibs/pylama/checkers/pylint/interfaces.py | 52 +-- pylibs/pylama/checkers/pylint/lint.py | 197 +++++----- .../checkers/pylint/logilab/astng/__init__.py | 72 ---- .../pylint/logilab/astng/brain/__init__.py | 0 .../logilab/astng/brain/py2mechanize.py | 20 - .../pylint/logilab/astng/brain/py2qt4.py | 25 -- .../pylint/logilab/astng/brain/py2stdlib.py | 182 --------- .../pylint/logilab/common/__pkginfo__.py | 9 +- .../pylint/logilab/common/changelog.py | 2 +- .../pylint/logilab/common/configuration.py | 15 +- .../pylint/logilab/common/deprecation.py | 234 +++++------ .../pylint/logilab/common/modutils.py | 33 +- .../pylint/logilab/common/optik_ext.py | 4 +- .../logilab/common/ureports/docbook_writer.py | 2 +- .../logilab/common/ureports/html_writer.py | 2 +- .../logilab/common/ureports/text_writer.py | 2 +- .../checkers/pylint/reporters/__init__.py | 62 +-- .../checkers/pylint/reporters/guireporter.py | 12 +- .../pylama/checkers/pylint/reporters/html.py | 17 +- .../pylama/checkers/pylint/reporters/text.py | 100 +++-- pylibs/pylama/checkers/pylint/utils.py | 151 +++++--- pylibs/pylama/core.py | 50 ++- pylibs/pylama/hook.py | 5 +- pylibs/pylama/inirama.py | 55 ++- pylibs/pylama/main.py | 3 +- pylibs/pylama/tasks.py | 3 +- pylibs/pylama/utils.py | 7 +- 68 files changed, 2120 insertions(+), 1781 deletions(-) create mode 100644 pylibs/pylama/checkers/pylint/astroid/__init__.py rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/__pkginfo__.py (71%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/as_string.py (78%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/bases.py (92%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/builder.py (83%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/exceptions.py (55%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/inference.py (88%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/manager.py (63%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/mixins.py (91%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/node_classes.py (89%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/nodes.py (88%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/protocols.py (96%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/raw_building.py (87%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/rebuilder.py (90%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/scoped_nodes.py (91%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/utils.py (90%) create mode 100644 pylibs/pylama/checkers/pylint/checkers/stdlib.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/__init__.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/brain/__init__.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/brain/py2mechanize.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/brain/py2qt4.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/brain/py2stdlib.py diff --git a/Changelog.rst b/Changelog.rst index 8a166c93..bf059336 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -6,6 +6,7 @@ Changelog * Added PEP257 checker; * Support 'pudb' in breakpoints; * Pyrun can now operate on a range of lines, and does not need to save (c) lawrenceakka +* Update pylama to version 1.5.0 ## 2013-05-15 0.6.18 -------------------- diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index 3d7ddc52..c3cb1d7b 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -1,11 +1,11 @@ -""" - Code audit tool for python. +""" Code audit tool for python. + +:copyright: 2013 by Kirill Klenov. +:license: BSD, see LICENSE for more details. - :copyright: 2013 by Kirill Klenov. - :license: BSD, see LICENSE for more details. """ -version_info = 1, 3, 3 +version_info = 1, 5, 0 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/checkers/mccabe.py b/pylibs/pylama/checkers/mccabe.py index 71f024a9..4ea34be0 100644 --- a/pylibs/pylama/checkers/mccabe.py +++ b/pylibs/pylama/checkers/mccabe.py @@ -3,17 +3,18 @@ http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html MIT License. """ -from __future__ import absolute_import, with_statement +from __future__ import with_statement -import sys - -import ast import optparse -from ast import iter_child_nodes +import sys from collections import defaultdict +try: + import ast + from ast import iter_child_nodes +except ImportError: # Python 2.5 + from flake8.util import ast, iter_child_nodes - -__version__ = '0.2' +__version__ = '0.2.1' class ASTVisitor(object): @@ -263,14 +264,13 @@ def get_code_complexity(code, threshold=7, filename='stdin'): complx = [] McCabeChecker.max_complexity = threshold - for lineno, offset, text, _ in McCabeChecker(tree, filename).run(): - complx.append(dict( - type=McCabeChecker._code, - lnum=lineno, - text=text, - )) + for lineno, offset, text, check in McCabeChecker(tree, filename).run(): + complx.append('%s:%d:1: %s' % (filename, lineno, text)) - return complx + if len(complx) == 0: + return 0 + print('\n'.join(complx)) + return len(complx) def get_module_complexity(module_path, threshold=7): @@ -310,5 +310,3 @@ def main(argv): if __name__ == '__main__': main(sys.argv[1:]) - -# lint=0 diff --git a/pylibs/pylama/checkers/pep257.py b/pylibs/pylama/checkers/pep257.py index aab6a91f..a43dbae7 100644 --- a/pylibs/pylama/checkers/pep257.py +++ b/pylibs/pylama/checkers/pep257.py @@ -54,6 +54,7 @@ def your_check(class_docstring, context, is_script): Also, see examples in "Check functions" section. """ +__version__ = '0.2.4' from curses.ascii import isascii import inspect @@ -150,6 +151,23 @@ def rel_pos(abs_pos, source): return len(lines) + 1, abs_pos - len(''.join(lines)) +def get_summary_line_info(thedocstring): + """Get the (summary_line, line_number) tuple for the given docstring. + + The returned 'summary_line' is the pep257 summary line and 'line_number' is + the zero-based docstring line number containing the summary line, which + will be either 0 (zeroth line) or 1 (first line). Any docstring checks + relating to the summary line should use this method to ensure consistent + treatment of the summary line. + + """ + lines = eval(thedocstring).split('\n') + first_line = lines[0].strip() + if len(lines) == 1 or len(first_line) > 0: + return first_line, 0 + return lines[1].strip(), 1 + + # # Parsing # @@ -167,8 +185,11 @@ def parse_module_docstring(source): def parse_docstring(source, what=''): """Parse docstring given `def` or `class` source.""" + module_docstring = parse_module_docstring(source) if what.startswith('module'): - return parse_module_docstring(source) + return module_docstring + if module_docstring: + return module_docstring token_gen = tk.generate_tokens(StringIO(source).readline) try: kind = None @@ -249,7 +270,7 @@ def parse_contexts(source, kind): if kind == 'def_docstring': return parse_functions(source) + parse_methods(source) if kind == 'docstring': - return ([source] + parse_functions(source) + + return ([parse_module_docstring(source)] + parse_functions(source) + parse_classes(source) + parse_methods(source)) @@ -378,7 +399,7 @@ def check_files(filenames): def parse_options(): - parser = OptionParser() + parser = OptionParser(version=__version__) parser.add_option('-e', '--explain', action='store_true', help='show explanation of each error') parser.add_option('-r', '--range', action='store_true', @@ -418,6 +439,7 @@ def main(options, arguments): f.close() for error in sorted(errors): print_error(str(error)) + return 1 if errors else 0 # @@ -546,7 +568,10 @@ def check_ends_with_period(docstring, context, is_script): The [first line of a] docstring is a phrase ending in a period. """ - if docstring and not eval(docstring).split('\n')[0].strip().endswith('.'): + if not docstring: + return + (summary_line, line_number) = get_summary_line_info(docstring) + if not summary_line.endswith('.'): return True @@ -610,8 +635,10 @@ def check_blank_after_summary(docstring, context, is_script): if not docstring: return lines = eval(docstring).split('\n') - if len(lines) > 1 and lines[1].strip() != '': - return True + if len(lines) > 1: + (summary_line, line_number) = get_summary_line_info(docstring) + if len(lines) <= (line_number+1) or lines[line_number+1].strip() != '': + return True def check_indent(docstring, context, is_script): @@ -645,7 +672,7 @@ def check_blank_before_after_class(class_docstring, context, is_script): """ if not class_docstring: return - before, after = context.split(class_docstring) + before, after = context.split(class_docstring)[:2] before_blanks = [not line.strip() for line in before.split('\n')] after_blanks = [not line.strip() for line in after.split('\n')] if before_blanks[-3:] != [False, True, True]: @@ -671,6 +698,6 @@ def check_blank_after_last_paragraph(docstring, context, is_script): if __name__ == '__main__': try: - main(*parse_options()) + sys.exit(main(*parse_options())) except KeyboardInterrupt: pass diff --git a/pylibs/pylama/checkers/pep8.py b/pylibs/pylama/checkers/pep8.py index 8413270f..e0035b3d 100644 --- a/pylibs/pylama/checkers/pep8.py +++ b/pylibs/pylama/checkers/pep8.py @@ -45,7 +45,7 @@ 700 statements 900 syntax error """ -__version__ = '1.4.6' +__version__ = '1.4.7a0' import os import sys @@ -1678,6 +1678,9 @@ def ignore_code(self, code): return False. Else, if 'options.ignore' contains a prefix of the error code, return True. """ + if len(code) < 4 and any(s.startswith(code) + for s in self.options.select): + return False return (code.startswith(self.options.ignore) and not code.startswith(self.options.select)) diff --git a/pylibs/pylama/checkers/pylint/__init__.py b/pylibs/pylama/checkers/pylint/__init__.py index 697c8c05..dfb4386b 100644 --- a/pylibs/pylama/checkers/pylint/__init__.py +++ b/pylibs/pylama/checkers/pylint/__init__.py @@ -17,7 +17,7 @@ def run_pylint(): """run pylint""" - from .lint import Run + from pylint.lint import Run Run(sys.argv[1:]) def run_pylint_gui(): @@ -40,6 +40,5 @@ def run_pyreverse(): def run_symilar(): """run symilar""" - from .checkers.similar import Run + from pylint.checkers.similar import Run Run(sys.argv[1:]) - diff --git a/pylibs/pylama/checkers/pylint/__pkginfo__.py b/pylibs/pylama/checkers/pylint/__pkginfo__.py index a018f934..997b9a59 100644 --- a/pylibs/pylama/checkers/pylint/__pkginfo__.py +++ b/pylibs/pylama/checkers/pylint/__pkginfo__.py @@ -18,10 +18,10 @@ modname = distname = 'pylint' -numversion = (0, 28, 0) +numversion = (1, 0, 0) version = '.'.join([str(num) for num in numversion]) -install_requires = ['logilab-common >= 0.53.0', 'logilab-astng >= 0.24.3'] +install_requires = ['logilab-common >= 0.53.0', 'astroid >= 0.24.3'] license = 'GPL' description = "python code static checker" @@ -66,3 +66,4 @@ for filename in ('pylint', 'pylint-gui', "symilar", "epylint", "pyreverse")] +include_dirs = ['test'] diff --git a/pylibs/pylama/checkers/pylint/astroid/__init__.py b/pylibs/pylama/checkers/pylint/astroid/__init__.py new file mode 100644 index 00000000..af17875d --- /dev/null +++ b/pylibs/pylama/checkers/pylint/astroid/__init__.py @@ -0,0 +1,118 @@ +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of astroid. +# +# astroid is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 2.1 of the License, or (at your +# option) any later version. +# +# astroid is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with astroid. If not, see . +"""Python Abstract Syntax Tree New Generation + +The aim of this module is to provide a common base representation of +python source code for projects such as pychecker, pyreverse, +pylint... Well, actually the development of this library is essentially +governed by pylint's needs. + +It extends class defined in the python's _ast module with some +additional methods and attributes. Instance attributes are added by a +builder object, which can either generate extended ast (let's call +them astroid ;) by visiting an existent ast tree or by inspecting living +object. Methods are added by monkey patching ast classes. + +Main modules are: + +* nodes and scoped_nodes for more information about methods and + attributes added to different node classes + +* the manager contains a high level object to get astroid trees from + source files and living objects. It maintains a cache of previously + constructed tree for quick access + +* builder contains the class responsible to build astroid trees +""" +__doctype__ = "restructuredtext en" + +import sys +import re +from operator import attrgetter + +# WARNING: internal imports order matters ! + +# make all exception classes accessible from astroid package +from .exceptions import * + +# make all node classes accessible from astroid package +from .nodes import * + +# trigger extra monkey-patching +from . import inference + +# more stuff available +from . import raw_building +from .bases import YES, Instance, BoundMethod, UnboundMethod +from .node_classes import are_exclusive, unpack_infer +from .scoped_nodes import builtin_lookup + +# make a manager instance (borg) as well as Project and Package classes +# accessible from astroid package +from .manager import AstroidManager, Project +MANAGER = AstroidManager() +del AstroidManager + +# transform utilities (filters and decorator) + +class AsStringRegexpPredicate(object): + """Class to be used as predicate that may be given to `register_transform` + + First argument is a regular expression that will be searched against the `as_string` + representation of the node onto which it's applied. + + If specified, the second argument is an `attrgetter` expression that will be + applied on the node first to get the actual node on which `as_string` should + be called. + """ + def __init__(self, regexp, expression=None): + self.regexp = re.compile(regexp) + self.expression = expression + + def __call__(self, node): + if self.expression is not None: + node = attrgetter(self.expression)(node) + return self.regexp.search(node.as_string()) + +def inference_tip(infer_function): + """Given an instance specific inference function, return a function to be + given to MANAGER.register_transform to set this inference function. + + Typical usage + + .. sourcecode:: python + + MANAGER.register_transform(CallFunc, inference_tip(infer_named_tuple), + AsStringRegexpPredicate('namedtuple', 'func')) + """ + def transform(node, infer_function=infer_function): + node._explicit_inference = infer_function + return node + return transform + +# load brain plugins +# from os import listdir +# from os.path import join, dirname +# BRAIN_MODULES_DIR = join(dirname(__file__), 'brain') +# if BRAIN_MODULES_DIR not in sys.path: + # # add it to the end of the list so user path take precedence + # sys.path.append(BRAIN_MODULES_DIR) +# load modules in this directory +# for module in listdir(BRAIN_MODULES_DIR): + # if module.endswith('.py'): + # __import__(module[:-3]) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/__pkginfo__.py b/pylibs/pylama/checkers/pylint/astroid/__pkginfo__.py similarity index 71% rename from pylibs/pylama/checkers/pylint/logilab/astng/__pkginfo__.py rename to pylibs/pylama/checkers/pylint/astroid/__pkginfo__.py index 31de45d0..a74b6b69 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/__pkginfo__.py +++ b/pylibs/pylama/checkers/pylint/astroid/__pkginfo__.py @@ -1,39 +1,37 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""logilab.astng packaging information""" +# with astroid. If not, see . +"""astroid packaging information""" -distname = 'logilab-astng' +distname = 'astroid' -modname = 'astng' -subpackage_of = 'logilab' +modname = 'astroid' -numversion = (0, 24, 3) +numversion = (1, 0, 0) version = '.'.join([str(num) for num in numversion]) -install_requires = ['logilab-common >= 0.53.0'] +install_requires = ['logilab-common >= 0.60.0'] license = 'LGPL' author = 'Logilab' author_email = 'python-projects@lists.logilab.org' mailinglist = "mailto://%s" % author_email -web = "http://www.logilab.org/project/%s" % distname -ftp = "ftp://ftp.logilab.org/pub/%s" % modname +web = 'http://bitbucket.org/logilab/astroid' description = "rebuild a new abstract syntax tree from Python's ast" diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/as_string.py b/pylibs/pylama/checkers/pylint/astroid/as_string.py similarity index 78% rename from pylibs/pylama/checkers/pylint/logilab/astng/as_string.py rename to pylibs/pylama/checkers/pylint/astroid/as_string.py index c21144e5..fcff19eb 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/as_string.py +++ b/pylibs/pylama/checkers/pylint/astroid/as_string.py @@ -1,21 +1,21 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""This module renders ASTNG nodes as string: +# with astroid. If not, see . +"""This module renders Astroid nodes as string: * :func:`to_code` function return equivalent (hopefuly valid) python string @@ -29,7 +29,7 @@ def dump(node, ids=False): - """print a nice astng tree representation. + """print a nice astroid tree representation. :param ids: if true, we also print the ids (usefull for debugging) """ @@ -41,7 +41,7 @@ def _repr_tree(node, result, indent='', _done=None, ids=False): """built a tree representation of a node as a list of lines""" if _done is None: _done = set() - if not hasattr(node, '_astng_fields'): # not a astng node + if not hasattr(node, '_astroid_fields'): # not a astroid node return if node in _done: result.append( indent + 'loop in tree: %s' % node ) @@ -52,7 +52,7 @@ def _repr_tree(node, result, indent='', _done=None, ids=False): node_str += ' . \t%x' % id(node) result.append( indent + node_str ) indent += INDENT - for field in node._astng_fields: + for field in node._astroid_fields: value = getattr(node, field) if isinstance(value, (list, tuple) ): result.append( indent + field + " = [" ) @@ -71,7 +71,7 @@ def _repr_tree(node, result, indent='', _done=None, ids=False): class AsStringVisitor(object): - """Visitor to render an ASTNG node as a valid python code string""" + """Visitor to render an Astroid node as a valid python code string""" def __call__(self, node): """Makes this visitor behave as a simple function""" @@ -86,52 +86,52 @@ def _stmt_list(self, stmts): ## visit_ methods ########################################### def visit_arguments(self, node): - """return an astng.Function node as string""" + """return an astroid.Function node as string""" return node.format_args() def visit_assattr(self, node): - """return an astng.AssAttr node as string""" + """return an astroid.AssAttr node as string""" return self.visit_getattr(node) def visit_assert(self, node): - """return an astng.Assert node as string""" + """return an astroid.Assert node as string""" if node.fail: return 'assert %s, %s' % (node.test.accept(self), node.fail.accept(self)) return 'assert %s' % node.test.accept(self) def visit_assname(self, node): - """return an astng.AssName node as string""" + """return an astroid.AssName node as string""" return node.name def visit_assign(self, node): - """return an astng.Assign node as string""" + """return an astroid.Assign node as string""" lhs = ' = '.join([n.accept(self) for n in node.targets]) return '%s = %s' % (lhs, node.value.accept(self)) def visit_augassign(self, node): - """return an astng.AugAssign node as string""" + """return an astroid.AugAssign node as string""" return '%s %s %s' % (node.target.accept(self), node.op, node.value.accept(self)) def visit_backquote(self, node): - """return an astng.Backquote node as string""" + """return an astroid.Backquote node as string""" return '`%s`' % node.value.accept(self) def visit_binop(self, node): - """return an astng.BinOp node as string""" + """return an astroid.BinOp node as string""" return '(%s) %s (%s)' % (node.left.accept(self), node.op, node.right.accept(self)) def visit_boolop(self, node): - """return an astng.BoolOp node as string""" + """return an astroid.BoolOp node as string""" return (' %s ' % node.op).join(['(%s)' % n.accept(self) for n in node.values]) def visit_break(self, node): - """return an astng.Break node as string""" + """return an astroid.Break node as string""" return 'break' def visit_callfunc(self, node): - """return an astng.CallFunc node as string""" + """return an astroid.CallFunc node as string""" expr_str = node.func.accept(self) args = [arg.accept(self) for arg in node.args] if node.starargs: @@ -141,7 +141,7 @@ def visit_callfunc(self, node): return '%s(%s)' % (expr_str, ', '.join(args)) def visit_class(self, node): - """return an astng.Class node as string""" + """return an astroid.Class node as string""" decorate = node.decorators and node.decorators.accept(self) or '' bases = ', '.join([n.accept(self) for n in node.bases]) bases = bases and '(%s)' % bases or '' @@ -150,54 +150,54 @@ def visit_class(self, node): self._stmt_list( node.body)) def visit_compare(self, node): - """return an astng.Compare node as string""" + """return an astroid.Compare node as string""" rhs_str = ' '.join(['%s %s' % (op, expr.accept(self)) for op, expr in node.ops]) return '%s %s' % (node.left.accept(self), rhs_str) def visit_comprehension(self, node): - """return an astng.Comprehension node as string""" + """return an astroid.Comprehension node as string""" ifs = ''.join([ ' if %s' % n.accept(self) for n in node.ifs]) return 'for %s in %s%s' % (node.target.accept(self), node.iter.accept(self), ifs ) def visit_const(self, node): - """return an astng.Const node as string""" + """return an astroid.Const node as string""" return repr(node.value) def visit_continue(self, node): - """return an astng.Continue node as string""" + """return an astroid.Continue node as string""" return 'continue' def visit_delete(self, node): # XXX check if correct - """return an astng.Delete node as string""" + """return an astroid.Delete node as string""" return 'del %s' % ', '.join([child.accept(self) for child in node.targets]) def visit_delattr(self, node): - """return an astng.DelAttr node as string""" + """return an astroid.DelAttr node as string""" return self.visit_getattr(node) def visit_delname(self, node): - """return an astng.DelName node as string""" + """return an astroid.DelName node as string""" return node.name def visit_decorators(self, node): - """return an astng.Decorators node as string""" + """return an astroid.Decorators node as string""" return '@%s\n' % '\n@'.join([item.accept(self) for item in node.nodes]) def visit_dict(self, node): - """return an astng.Dict node as string""" + """return an astroid.Dict node as string""" return '{%s}' % ', '.join(['%s: %s' % (key.accept(self), value.accept(self)) for key, value in node.items]) def visit_dictcomp(self, node): - """return an astng.DictComp node as string""" + """return an astroid.DictComp node as string""" return '{%s: %s %s}' % (node.key.accept(self), node.value.accept(self), ' '.join([n.accept(self) for n in node.generators])) def visit_discard(self, node): - """return an astng.Discard node as string""" + """return an astroid.Discard node as string""" return node.value.accept(self) def visit_emptynode(self, node): @@ -216,7 +216,7 @@ def visit_excepthandler(self, node): return '%s:\n%s' % (excs, self._stmt_list(node.body)) def visit_ellipsis(self, node): - """return an astng.Ellipsis node as string""" + """return an astroid.Ellipsis node as string""" return '...' def visit_empty(self, node): @@ -224,7 +224,7 @@ def visit_empty(self, node): return '' def visit_exec(self, node): - """return an astng.Exec node as string""" + """return an astroid.Exec node as string""" if node.locals: return 'exec %s in %s, %s' % (node.expr.accept(self), node.locals.accept(self), @@ -235,11 +235,11 @@ def visit_exec(self, node): return 'exec %s' % node.expr.accept(self) def visit_extslice(self, node): - """return an astng.ExtSlice node as string""" + """return an astroid.ExtSlice node as string""" return ','.join( [dim.accept(self) for dim in node.dims] ) def visit_for(self, node): - """return an astng.For node as string""" + """return an astroid.For node as string""" fors = 'for %s in %s:\n%s' % (node.target.accept(self), node.iter.accept(self), self._stmt_list( node.body)) @@ -248,78 +248,78 @@ def visit_for(self, node): return fors def visit_from(self, node): - """return an astng.From node as string""" + """return an astroid.From node as string""" return 'from %s import %s' % ('.' * (node.level or 0) + node.modname, _import_string(node.names)) def visit_function(self, node): - """return an astng.Function node as string""" + """return an astroid.Function node as string""" decorate = node.decorators and node.decorators.accept(self) or '' docs = node.doc and '\n%s"""%s"""' % (INDENT, node.doc) or '' return '\n%sdef %s(%s):%s\n%s' % (decorate, node.name, node.args.accept(self), docs, self._stmt_list(node.body)) def visit_genexpr(self, node): - """return an astng.GenExpr node as string""" + """return an astroid.GenExpr node as string""" return '(%s %s)' % (node.elt.accept(self), ' '.join([n.accept(self) for n in node.generators])) def visit_getattr(self, node): - """return an astng.Getattr node as string""" + """return an astroid.Getattr node as string""" return '%s.%s' % (node.expr.accept(self), node.attrname) def visit_global(self, node): - """return an astng.Global node as string""" + """return an astroid.Global node as string""" return 'global %s' % ', '.join(node.names) def visit_if(self, node): - """return an astng.If node as string""" + """return an astroid.If node as string""" ifs = ['if %s:\n%s' % (node.test.accept(self), self._stmt_list(node.body))] if node.orelse:# XXX use elif ??? ifs.append('else:\n%s' % self._stmt_list(node.orelse)) return '\n'.join(ifs) def visit_ifexp(self, node): - """return an astng.IfExp node as string""" + """return an astroid.IfExp node as string""" return '%s if %s else %s' % (node.body.accept(self), node.test.accept(self), node.orelse.accept(self)) def visit_import(self, node): - """return an astng.Import node as string""" + """return an astroid.Import node as string""" return 'import %s' % _import_string(node.names) def visit_keyword(self, node): - """return an astng.Keyword node as string""" + """return an astroid.Keyword node as string""" return '%s=%s' % (node.arg, node.value.accept(self)) def visit_lambda(self, node): - """return an astng.Lambda node as string""" + """return an astroid.Lambda node as string""" return 'lambda %s: %s' % (node.args.accept(self), node.body.accept(self)) def visit_list(self, node): - """return an astng.List node as string""" + """return an astroid.List node as string""" return '[%s]' % ', '.join([child.accept(self) for child in node.elts]) def visit_listcomp(self, node): - """return an astng.ListComp node as string""" + """return an astroid.ListComp node as string""" return '[%s %s]' % (node.elt.accept(self), ' '.join([n.accept(self) for n in node.generators])) def visit_module(self, node): - """return an astng.Module node as string""" + """return an astroid.Module node as string""" docs = node.doc and '"""%s"""\n\n' % node.doc or '' return docs + '\n'.join([n.accept(self) for n in node.body]) + '\n\n' def visit_name(self, node): - """return an astng.Name node as string""" + """return an astroid.Name node as string""" return node.name def visit_pass(self, node): - """return an astng.Pass node as string""" + """return an astroid.Pass node as string""" return 'pass' def visit_print(self, node): - """return an astng.Print node as string""" + """return an astroid.Print node as string""" nodes = ', '.join([n.accept(self) for n in node.values]) if not node.nl: nodes = '%s,' % nodes @@ -328,7 +328,7 @@ def visit_print(self, node): return 'print %s' % nodes def visit_raise(self, node): - """return an astng.Raise node as string""" + """return an astroid.Raise node as string""" if node.exc: if node.inst: if node.tback: @@ -341,27 +341,27 @@ def visit_raise(self, node): return 'raise' def visit_return(self, node): - """return an astng.Return node as string""" + """return an astroid.Return node as string""" if node.value: return 'return %s' % node.value.accept(self) else: return 'return' def visit_index(self, node): - """return a astng.Index node as string""" + """return a astroid.Index node as string""" return node.value.accept(self) def visit_set(self, node): - """return an astng.Set node as string""" + """return an astroid.Set node as string""" return '{%s}' % ', '.join([child.accept(self) for child in node.elts]) def visit_setcomp(self, node): - """return an astng.SetComp node as string""" + """return an astroid.SetComp node as string""" return '{%s %s}' % (node.elt.accept(self), ' '.join([n.accept(self) for n in node.generators])) def visit_slice(self, node): - """return a astng.Slice node as string""" + """return a astroid.Slice node as string""" lower = node.lower and node.lower.accept(self) or '' upper = node.upper and node.upper.accept(self) or '' step = node.step and node.step.accept(self) or '' @@ -370,11 +370,11 @@ def visit_slice(self, node): return '%s:%s' % (lower, upper) def visit_subscript(self, node): - """return an astng.Subscript node as string""" + """return an astroid.Subscript node as string""" return '%s[%s]' % (node.value.accept(self), node.slice.accept(self)) def visit_tryexcept(self, node): - """return an astng.TryExcept node as string""" + """return an astroid.TryExcept node as string""" trys = ['try:\n%s' % self._stmt_list( node.body)] for handler in node.handlers: trys.append(handler.accept(self)) @@ -383,16 +383,16 @@ def visit_tryexcept(self, node): return '\n'.join(trys) def visit_tryfinally(self, node): - """return an astng.TryFinally node as string""" + """return an astroid.TryFinally node as string""" return 'try:\n%s\nfinally:\n%s' % (self._stmt_list( node.body), self._stmt_list(node.finalbody)) def visit_tuple(self, node): - """return an astng.Tuple node as string""" + """return an astroid.Tuple node as string""" return '(%s)' % ', '.join([child.accept(self) for child in node.elts]) def visit_unaryop(self, node): - """return an astng.UnaryOp node as string""" + """return an astroid.UnaryOp node as string""" if node.op == 'not': operator = 'not ' else: @@ -400,7 +400,7 @@ def visit_unaryop(self, node): return '%s%s' % (operator, node.operand.accept(self)) def visit_while(self, node): - """return an astng.While node as string""" + """return an astroid.While node as string""" whiles = 'while %s:\n%s' % (node.test.accept(self), self._stmt_list(node.body)) if node.orelse: @@ -408,11 +408,11 @@ def visit_while(self, node): return whiles def visit_with(self, node): # 'with' without 'as' is possible - """return an astng.With node as string""" - as_var = node.vars and " as (%s)" % (node.vars.accept(self)) or "" - withs = 'with (%s)%s:\n%s' % (node.expr.accept(self), as_var, - self._stmt_list( node.body)) - return withs + """return an astroid.With node as string""" + items = ', '.join(('(%s)' % expr.accept(self)) + + (vars and ' as (%s)' % (vars.accept(self)) or '') + for expr, vars in node.items) + return 'with %s:\n%s' % (items, self._stmt_list( node.body)) def visit_yield(self, node): """yield an ast.Yield node as string""" @@ -439,11 +439,11 @@ def visit_excepthandler(self, node): return '%s:\n%s' % (excs, self._stmt_list(node.body)) def visit_nonlocal(self, node): - """return an astng.Nonlocal node as string""" + """return an astroid.Nonlocal node as string""" return 'nonlocal %s' % ', '.join(node.names) def visit_raise(self, node): - """return an astng.Raise node as string""" + """return an astroid.Raise node as string""" if node.exc: if node.cause: return 'raise %s from %s' % (node.exc.accept(self), diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/bases.py b/pylibs/pylama/checkers/pylint/astroid/bases.py similarity index 92% rename from pylibs/pylama/checkers/pylint/logilab/astng/bases.py rename to pylibs/pylama/checkers/pylint/astroid/bases.py index 6331dc8e..641f88ae 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/bases.py +++ b/pylibs/pylama/checkers/pylint/astroid/bases.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """This module contains base classes and functions for the nodes and some inference utils. """ @@ -24,8 +24,8 @@ import sys from contextlib import contextmanager -from .exceptions import (InferenceError, ASTNGError, - NotFoundError, UnresolvableName) +from .exceptions import (InferenceError, AstroidError, NotFoundError, + UnresolvableName, UseInferenceDefault) if sys.version_info >= (3, 0): @@ -339,7 +339,7 @@ def wrapper(*args, **kwargs): # Node ###################################################################### class NodeNG(object): - """Base Class for all ASTNG node classes. + """Base Class for all Astroid node classes. It represents a node of the new abstract syntax tree. """ @@ -354,7 +354,24 @@ class NodeNG(object): # parent node in the tree parent = None # attributes containing child node(s) redefined in most concrete classes: - _astng_fields = () + _astroid_fields = () + # instance specific inference function infer(node, context) + _explicit_inference = None + + def infer(self, context=None, **kwargs): + """main interface to the interface system, return a generator on infered + values. + + If the instance has some explicit inference function set, it will be + called instead of the default interface. + """ + if self._explicit_inference is not None: + # explicit_inference is not bound, give it self explicitly + try: + return self._explicit_inference(self, context, **kwargs) + except UseInferenceDefault: + pass + return self._infer(context, **kwargs) def _repr_name(self): """return self.name or self.attrname or '' for nice representation""" @@ -377,7 +394,7 @@ def accept(self, visitor): return func(self) def get_children(self): - for field in self._astng_fields: + for field in self._astroid_fields: attr = getattr(self, field) if attr is None: continue @@ -389,7 +406,7 @@ def get_children(self): def last_child(self): """an optimized version of list(get_children())[-1]""" - for field in self._astng_fields[::-1]: + for field in self._astroid_fields[::-1]: attr = getattr(self, field) if not attr: # None or empty listy / tuple continue @@ -433,7 +450,7 @@ def root(self): def child_sequence(self, child): """search for the right sequence where the child lies in""" - for field in self._astng_fields: + for field in self._astroid_fields: node_or_sequence = getattr(self, field) if node_or_sequence is child: return [node_or_sequence] @@ -441,20 +458,20 @@ def child_sequence(self, child): if isinstance(node_or_sequence, (tuple, list)) and child in node_or_sequence: return node_or_sequence else: - msg = 'Could not found %s in %s\'s children' - raise ASTNGError(msg % (repr(child), repr(self))) + msg = 'Could not find %s in %s\'s children' + raise AstroidError(msg % (repr(child), repr(self))) def locate_child(self, child): """return a 2-uple (child attribute name, sequence or node)""" - for field in self._astng_fields: + for field in self._astroid_fields: node_or_sequence = getattr(self, field) # /!\ compiler.ast Nodes have an __iter__ walking over child nodes if child is node_or_sequence: return field, child if isinstance(node_or_sequence, (tuple, list)) and child in node_or_sequence: return field, node_or_sequence - msg = 'Could not found %s in %s\'s children' - raise ASTNGError(msg % (repr(child), repr(self))) + msg = 'Could not find %s in %s\'s children' + raise AstroidError(msg % (repr(child), repr(self))) # FIXME : should we merge child_sequence and locate_child ? locate_child # is only used in are_exclusive, child_sequence one time in pylint. @@ -543,7 +560,7 @@ def _infer_name(self, frame, name): # overridden for From, Import, Global, TryExcept and Arguments return None - def infer(self, context=None): + def _infer(self, context=None): """we don't know how to resolve a statement by default""" # this method is overridden by most concrete classes raise InferenceError(self.__class__.__name__) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/builder.py b/pylibs/pylama/checkers/pylint/astroid/builder.py similarity index 83% rename from pylibs/pylama/checkers/pylint/logilab/astng/builder.py rename to pylibs/pylama/checkers/pylint/astroid/builder.py index ae0c7d01..6a4347af 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/builder.py +++ b/pylibs/pylama/checkers/pylint/astroid/builder.py @@ -1,21 +1,21 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""The ASTNGBuilder makes astng from living object and / or from _ast +# with astroid. If not, see . +"""The AstroidBuilder makes astroid from living object and / or from _ast The builder is not thread safe and can't be used to parse different sources at the same time. @@ -26,12 +26,12 @@ import sys from os.path import splitext, basename, exists, abspath -from ..common.modutils import modpath_from_file +from ..logilab.common.modutils import modpath_from_file -from .exceptions import ASTNGBuildingException, InferenceError +from .exceptions import AstroidBuildingException, InferenceError from .raw_building import InspectBuilder from .rebuilder import TreeRebuilder -from .manager import ASTNGManager +from .manager import AstroidManager from .bases import YES, Instance from _ast import PyCF_ONLY_AST @@ -50,7 +50,7 @@ def open_source_file(filename): except UnicodeError, uex: # wrong encodingg # detect_encoding returns utf-8 if no encoding specified msg = 'Wrong (%s) or no encoding specified' % encoding - raise ASTNGBuildingException(msg) + raise AstroidBuildingException(msg) return stream, encoding, data else: @@ -79,18 +79,17 @@ def open_source_file(filename): # ast NG builder ############################################################## -MANAGER = ASTNGManager() +MANAGER = AstroidManager() -class ASTNGBuilder(InspectBuilder): - """provide astng building methods""" - rebuilder = TreeRebuilder() +class AstroidBuilder(InspectBuilder): + """provide astroid building methods""" def __init__(self, manager=None): InspectBuilder.__init__(self) self._manager = manager or MANAGER def module_build(self, module, modname=None): - """build an astng from a living module instance + """build an astroid from a living module instance """ node = None path = getattr(module, '__file__', None) @@ -105,7 +104,7 @@ def module_build(self, module, modname=None): return node def file_build(self, path, modname=None): - """build astng from a source code file (i.e. from an ast) + """build astroid from a source code file (i.e. from an ast) path is expected to be a python source file """ @@ -113,35 +112,32 @@ def file_build(self, path, modname=None): stream, encoding, data = open_source_file(path) except IOError, exc: msg = 'Unable to load file %r (%s)' % (path, exc) - raise ASTNGBuildingException(msg) + raise AstroidBuildingException(msg) except SyntaxError, exc: # py3k encoding specification error - raise ASTNGBuildingException(exc) + raise AstroidBuildingException(exc) except LookupError, exc: # unknown encoding - raise ASTNGBuildingException(exc) + raise AstroidBuildingException(exc) # get module name if necessary if modname is None: try: modname = '.'.join(modpath_from_file(path)) except ImportError: modname = splitext(basename(path))[0] - # build astng representation + # build astroid representation node = self.string_build(data, modname, path) node.file_encoding = encoding return node def string_build(self, data, modname='', path=None): - """build astng from source code string and return rebuilded astng""" + """build astroid from source code string and return rebuilded astroid""" module = self._data_build(data, modname, path) - self._manager.astng_cache[module.name] = module + self._manager.astroid_cache[module.name] = module # post tree building steps after we stored the module in the cache: for from_node in module._from_nodes: self.add_from_names_to_locals(from_node) # handle delayed assattr nodes for delayed in module._delayed_assattr: self.delayed_assattr(delayed) - if modname: - for transformer in self._manager.transformers: - transformer(module) return module def _data_build(self, data, modname, path): @@ -157,11 +153,11 @@ def _data_build(self, data, modname, path): package = True else: package = path and path.find('__init__.py') > -1 or False - self.rebuilder.init() - module = self.rebuilder.visit_module(node, modname, package) + rebuilder = TreeRebuilder(self._manager) + module = rebuilder.visit_module(node, modname, package) module.file = module.path = node_file - module._from_nodes = self.rebuilder._from_nodes - module._delayed_assattr = self.rebuilder._delayed_assattr + module._from_nodes = rebuilder._from_nodes + module._delayed_assattr = rebuilder._delayed_assattr return module def add_from_names_to_locals(self, node): @@ -176,7 +172,7 @@ def sort_locals(my_list): if name == '*': try: imported = node.root().import_module(node.modname) - except ASTNGBuildingException: + except AstroidBuildingException: continue for name in imported.wildcard_import_names(): node.parent.set_local(name, node) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/exceptions.py b/pylibs/pylama/checkers/pylint/astroid/exceptions.py similarity index 55% rename from pylibs/pylama/checkers/pylint/logilab/astng/exceptions.py rename to pylibs/pylama/checkers/pylint/astroid/exceptions.py index db33f8b9..3889e2e7 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/exceptions.py +++ b/pylibs/pylama/checkers/pylint/astroid/exceptions.py @@ -1,34 +1,34 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""this module contains exceptions used in the astng library +# with astroid. If not, see . +"""this module contains exceptions used in the astroid library """ __doctype__ = "restructuredtext en" -class ASTNGError(Exception): - """base exception class for all astng related exceptions""" +class AstroidError(Exception): + """base exception class for all astroid related exceptions""" -class ASTNGBuildingException(ASTNGError): - """exception class when we are unable to build an astng representation""" +class AstroidBuildingException(AstroidError): + """exception class when we are unable to build an astroid representation""" -class ResolveError(ASTNGError): - """base class of astng resolution/inference error""" +class ResolveError(AstroidError): + """base class of astroid resolution/inference error""" class NotFoundError(ResolveError): """raised when we are unable to resolve a name""" @@ -36,10 +36,15 @@ class NotFoundError(ResolveError): class InferenceError(ResolveError): """raised when we are unable to infer a node""" +class UseInferenceDefault(Exception): + """exception to be raised in custom inference function to indicate that it + should go back to the default behaviour + """ + class UnresolvableName(InferenceError): """raised when we are unable to resolve a name""" -class NoDefault(ASTNGError): +class NoDefault(AstroidError): """raised by function's `default_value` method when an argument has no default value """ diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/inference.py b/pylibs/pylama/checkers/pylint/astroid/inference.py similarity index 88% rename from pylibs/pylama/checkers/pylint/logilab/astng/inference.py rename to pylibs/pylama/checkers/pylint/astroid/inference.py index fda25cf8..f600781c 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/inference.py +++ b/pylibs/pylama/checkers/pylint/astroid/inference.py @@ -1,21 +1,21 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""this module contains a set of functions to handle inference on astng trees +# with astroid. If not, see . +"""this module contains a set of functions to handle inference on astroid trees """ __doctype__ = "restructuredtext en" @@ -24,14 +24,14 @@ from . import nodes -from .manager import ASTNGManager -from .exceptions import (ASTNGError, +from .manager import AstroidManager +from .exceptions import (AstroidError, InferenceError, NoDefault, NotFoundError, UnresolvableName) from .bases import YES, Instance, InferenceContext, \ _infer_stmts, copy_context, path_wrapper, raise_if_nothing_infered from .protocols import _arguments_infer_argname -MANAGER = ASTNGManager() +MANAGER = AstroidManager() class CallContext: @@ -55,7 +55,7 @@ def infer_argument(self, funcnode, name, context): try: return self.nargs[name].infer(context) except KeyError: - # Function.args.args can be None in astng (means that we don't have + # Function.args.args can be None in astroid (means that we don't have # information on argnames) argindex = funcnode.args.find_argname(name)[0] if argindex is not None: @@ -126,15 +126,15 @@ def infer_end(self, context=None): """inference's end for node such as Module, Class, Function, Const... """ yield self -nodes.Module.infer = infer_end -nodes.Class.infer = infer_end -nodes.Function.infer = infer_end -nodes.Lambda.infer = infer_end -nodes.Const.infer = infer_end -nodes.List.infer = infer_end -nodes.Tuple.infer = infer_end -nodes.Dict.infer = infer_end -nodes.Set.infer = infer_end +nodes.Module._infer = infer_end +nodes.Class._infer = infer_end +nodes.Function._infer = infer_end +nodes.Lambda._infer = infer_end +nodes.Const._infer = infer_end +nodes.List._infer = infer_end +nodes.Tuple._infer = infer_end +nodes.Dict._infer = infer_end +nodes.Set._infer = infer_end def infer_name(self, context=None): """infer a Name: use name lookup rules""" @@ -144,7 +144,7 @@ def infer_name(self, context=None): context = context.clone() context.lookupname = self.name return _infer_stmts(stmts, context, frame) -nodes.Name.infer = path_wrapper(infer_name) +nodes.Name._infer = path_wrapper(infer_name) nodes.AssName.infer_lhs = infer_name # won't work with a path wrapper @@ -164,7 +164,7 @@ def infer_callfunc(self, context=None): except InferenceError: ## XXX log error ? continue -nodes.CallFunc.infer = path_wrapper(raise_if_nothing_infered(infer_callfunc)) +nodes.CallFunc._infer = path_wrapper(raise_if_nothing_infered(infer_callfunc)) def infer_import(self, context=None, asname=True): @@ -176,7 +176,7 @@ def infer_import(self, context=None, asname=True): yield self.do_import_module(self.real_name(name)) else: yield self.do_import_module(name) -nodes.Import.infer = path_wrapper(infer_import) +nodes.Import._infer = path_wrapper(infer_import) def infer_name_module(self, name): context = InferenceContext() @@ -199,7 +199,7 @@ def infer_from(self, context=None, asname=True): return _infer_stmts(module.getattr(name, ignore_locals=module is self.root()), context) except NotFoundError: raise InferenceError(name) -nodes.From.infer = path_wrapper(infer_from) +nodes.From._infer = path_wrapper(infer_from) def infer_getattr(self, context=None): @@ -219,7 +219,7 @@ def infer_getattr(self, context=None): except AttributeError: # XXX method / function context.boundnode = None -nodes.Getattr.infer = path_wrapper(raise_if_nothing_infered(infer_getattr)) +nodes.Getattr._infer = path_wrapper(raise_if_nothing_infered(infer_getattr)) nodes.AssAttr.infer_lhs = raise_if_nothing_infered(infer_getattr) # # won't work with a path wrapper @@ -230,7 +230,7 @@ def infer_global(self, context=None): return _infer_stmts(self.root().getattr(context.lookupname), context) except NotFoundError: raise InferenceError() -nodes.Global.infer = path_wrapper(infer_global) +nodes.Global._infer = path_wrapper(infer_global) def infer_subscript(self, context=None): @@ -257,7 +257,7 @@ def infer_subscript(self, context=None): yield infered else: raise InferenceError() -nodes.Subscript.infer = path_wrapper(infer_subscript) +nodes.Subscript._infer = path_wrapper(infer_subscript) nodes.Subscript.infer_lhs = raise_if_nothing_infered(infer_subscript) @@ -287,7 +287,7 @@ def infer_unaryop(self, context=None): raise except: yield YES -nodes.UnaryOp.infer = path_wrapper(infer_unaryop) +nodes.UnaryOp._infer = path_wrapper(infer_unaryop) BIN_OP_METHOD = {'+': '__add__', @@ -332,7 +332,7 @@ def infer_binop(self, context=None): for rhs in self.right.infer(context): for val in _infer_binop(self.op, rhs, lhs, context): yield val -nodes.BinOp.infer = path_wrapper(infer_binop) +nodes.BinOp._infer = path_wrapper(infer_binop) def infer_arguments(self, context=None): @@ -340,7 +340,7 @@ def infer_arguments(self, context=None): if name is None: raise InferenceError() return _arguments_infer_argname(self, name, context) -nodes.Arguments.infer = infer_arguments +nodes.Arguments._infer = infer_arguments def infer_ass(self, context=None): @@ -352,8 +352,8 @@ def infer_ass(self, context=None): return stmt.infer(context) stmts = list(self.assigned_stmts(context=context)) return _infer_stmts(stmts, context) -nodes.AssName.infer = path_wrapper(infer_ass) -nodes.AssAttr.infer = path_wrapper(infer_ass) +nodes.AssName._infer = path_wrapper(infer_ass) +nodes.AssAttr._infer = path_wrapper(infer_ass) def infer_augassign(self, context=None): failures = [] @@ -364,7 +364,7 @@ def infer_augassign(self, context=None): for rhs in self.value.infer(context): for val in _infer_binop(self.op, rhs, lhs, context): yield val -nodes.AugAssign.infer = path_wrapper(infer_augassign) +nodes.AugAssign._infer = path_wrapper(infer_augassign) # no infer method on DelName and DelAttr (expected InferenceError) @@ -375,14 +375,14 @@ def infer_empty_node(self, context=None): yield YES else: try: - for infered in MANAGER.infer_astng_from_something(self.object, + for infered in MANAGER.infer_ast_from_something(self.object, context=context): yield infered - except ASTNGError: + except AstroidError: yield YES -nodes.EmptyNode.infer = path_wrapper(infer_empty_node) +nodes.EmptyNode._infer = path_wrapper(infer_empty_node) def infer_index(self, context=None): return self.value.infer(context) -nodes.Index.infer = infer_index +nodes.Index._infer = infer_index diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/manager.py b/pylibs/pylama/checkers/pylint/astroid/manager.py similarity index 63% rename from pylibs/pylama/checkers/pylint/logilab/astng/manager.py rename to pylibs/pylama/checkers/pylint/astroid/manager.py index d090f80b..3ff2e14c 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/manager.py +++ b/pylibs/pylama/checkers/pylint/astroid/manager.py @@ -1,22 +1,22 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""astng manager: avoid multiple astng build of a same module when -possible by providing a class responsible to get astng representation +# with astroid. If not, see . +"""astroid manager: avoid multiple astroid build of a same module when +possible by providing a class responsible to get astroid representation from various source and using a cache of built modules) """ @@ -25,19 +25,19 @@ import os from os.path import dirname, join, isdir, exists -from ..common.modutils import NoSourceFile, is_python_source, \ +from ..logilab.common.modutils import NoSourceFile, is_python_source, \ file_from_modpath, load_module_from_name, modpath_from_file, \ get_module_files, get_source_file, zipimport -from ..common.configuration import OptionsProviderMixIn +from ..logilab.common.configuration import OptionsProviderMixIn -from .exceptions import ASTNGBuildingException +from .exceptions import AstroidBuildingException -def astng_wrapper(func, modname): - """wrapper to give to ASTNGManager.project_from_files""" +def astroid_wrapper(func, modname): + """wrapper to give to AstroidManager.project_from_files""" print 'parsing %s...' % modname try: return func(modname) - except ASTNGBuildingException, exc: + except AstroidBuildingException, exc: print exc except Exception, exc: import traceback @@ -55,14 +55,14 @@ def safe_repr(obj): -class ASTNGManager(OptionsProviderMixIn): - """the astng manager, responsible to build astng from files +class AstroidManager(OptionsProviderMixIn): + """the astroid manager, responsible to build astroid from files or modules. Use the Borg pattern. """ - name = 'astng loader' + name = 'astroid loader' options = (("ignore", {'type' : "csv", 'metavar' : "", 'dest' : "black_list", "default" : ('CVS',), @@ -76,17 +76,17 @@ class ASTNGManager(OptionsProviderMixIn): ) brain = {} def __init__(self): - self.__dict__ = ASTNGManager.brain + self.__dict__ = AstroidManager.brain if not self.__dict__: OptionsProviderMixIn.__init__(self) self.load_defaults() # NOTE: cache entries are added by the [re]builder - self.astng_cache = {} + self.astroid_cache = {} self._mod_file_cache = {} - self.transformers = [] + self.transforms = {} - def astng_from_file(self, filepath, modname=None, fallback=True, source=False): - """given a module name, return the astng object""" + def ast_from_file(self, filepath, modname=None, fallback=True, source=False): + """given a module name, return the astroid object""" try: filepath = get_source_file(filepath, include_no_ext=True) source = True @@ -97,23 +97,23 @@ def astng_from_file(self, filepath, modname=None, fallback=True, source=False): modname = '.'.join(modpath_from_file(filepath)) except ImportError: modname = filepath - if modname in self.astng_cache: - return self.astng_cache[modname] + if modname in self.astroid_cache: + return self.astroid_cache[modname] if source: - from .builder import ASTNGBuilder - return ASTNGBuilder(self).file_build(filepath, modname) + from .builder import AstroidBuilder + return AstroidBuilder(self).file_build(filepath, modname) elif fallback and modname: - return self.astng_from_module_name(modname) - raise ASTNGBuildingException('unable to get astng for file %s' % + return self.ast_from_module_name(modname) + raise AstroidBuildingException('unable to get astroid for file %s' % filepath) - def astng_from_module_name(self, modname, context_file=None): - """given a module name, return the astng object""" - if modname in self.astng_cache: - return self.astng_cache[modname] + def ast_from_module_name(self, modname, context_file=None): + """given a module name, return the astroid object""" + if modname in self.astroid_cache: + return self.astroid_cache[modname] if modname == '__main__': - from .builder import ASTNGBuilder - return ASTNGBuilder(self).string_build('', modname) + from .builder import AstroidBuilder + return AstroidBuilder(self).string_build('', modname) old_cwd = os.getcwd() if context_file: os.chdir(dirname(context_file)) @@ -128,17 +128,17 @@ def astng_from_module_name(self, modname, context_file=None): module = load_module_from_name(modname) except Exception, ex: msg = 'Unable to load module %s (%s)' % (modname, ex) - raise ASTNGBuildingException(msg) - return self.astng_from_module(module, modname) - return self.astng_from_file(filepath, modname, fallback=False) + raise AstroidBuildingException(msg) + return self.ast_from_module(module, modname) + return self.ast_from_file(filepath, modname, fallback=False) finally: os.chdir(old_cwd) def zip_import_data(self, filepath): if zipimport is None: return None - from .builder import ASTNGBuilder - builder = ASTNGBuilder(self) + from .builder import AstroidBuilder + builder = AstroidBuilder(self) for ext in ('.zip', '.egg'): try: eggpath, resource = filepath.rsplit(ext + '/', 1) @@ -165,41 +165,41 @@ def file_from_module_name(self, modname, contextfile): context_file=contextfile) except ImportError, ex: msg = 'Unable to load module %s (%s)' % (modname, ex) - value = ASTNGBuildingException(msg) + value = AstroidBuildingException(msg) self._mod_file_cache[(modname, contextfile)] = value - if isinstance(value, ASTNGBuildingException): + if isinstance(value, AstroidBuildingException): raise value return value - def astng_from_module(self, module, modname=None): - """given an imported module, return the astng object""" + def ast_from_module(self, module, modname=None): + """given an imported module, return the astroid object""" modname = modname or module.__name__ - if modname in self.astng_cache: - return self.astng_cache[modname] + if modname in self.astroid_cache: + return self.astroid_cache[modname] try: # some builtin modules don't have __file__ attribute filepath = module.__file__ if is_python_source(filepath): - return self.astng_from_file(filepath, modname) + return self.ast_from_file(filepath, modname) except AttributeError: pass - from .builder import ASTNGBuilder - return ASTNGBuilder(self).module_build(module, modname) + from .builder import AstroidBuilder + return AstroidBuilder(self).module_build(module, modname) - def astng_from_class(self, klass, modname=None): - """get astng for the given class""" + def ast_from_class(self, klass, modname=None): + """get astroid for the given class""" if modname is None: try: modname = klass.__module__ except AttributeError: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unable to get module for class %s' % safe_repr(klass)) - modastng = self.astng_from_module_name(modname) - return modastng.getattr(klass.__name__)[0] # XXX + modastroid = self.ast_from_module_name(modname) + return modastroid.getattr(klass.__name__)[0] # XXX - def infer_astng_from_something(self, obj, context=None): - """infer astng for the given class""" + def infer_ast_from_something(self, obj, context=None): + """infer astroid for the given class""" if hasattr(obj, '__class__') and not isinstance(obj, type): klass = obj.__class__ else: @@ -207,31 +207,31 @@ def infer_astng_from_something(self, obj, context=None): try: modname = klass.__module__ except AttributeError: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unable to get module for %s' % safe_repr(klass)) except Exception, ex: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unexpected error while retrieving module for %s: %s' % (safe_repr(klass), ex)) try: name = klass.__name__ except AttributeError: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unable to get name for %s' % safe_repr(klass)) except Exception, ex: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unexpected error while retrieving name for %s: %s' % (safe_repr(klass), ex)) # take care, on living object __module__ is regularly wrong :( - modastng = self.astng_from_module_name(modname) + modastroid = self.ast_from_module_name(modname) if klass is obj: - for infered in modastng.igetattr(name, context): + for infered in modastroid.igetattr(name, context): yield infered else: - for infered in modastng.igetattr(name, context): + for infered in modastroid.igetattr(name, context): yield infered.instanciate_class() - def project_from_files(self, files, func_wrapper=astng_wrapper, + def project_from_files(self, files, func_wrapper=astroid_wrapper, project_name=None, black_list=None): """return a Project from a list of files or modules""" # build the project representation @@ -245,26 +245,33 @@ def project_from_files(self, files, func_wrapper=astng_wrapper, fpath = join(something, '__init__.py') else: fpath = something - astng = func_wrapper(self.astng_from_file, fpath) - if astng is None: + astroid = func_wrapper(self.ast_from_file, fpath) + if astroid is None: continue # XXX why is first file defining the project.path ? - project.path = project.path or astng.file - project.add_module(astng) - base_name = astng.name + project.path = project.path or astroid.file + project.add_module(astroid) + base_name = astroid.name # recurse in package except if __init__ was explicitly given - if astng.package and something.find('__init__') == -1: + if astroid.package and something.find('__init__') == -1: # recurse on others packages / modules if this is a package - for fpath in get_module_files(dirname(astng.file), + for fpath in get_module_files(dirname(astroid.file), black_list): - astng = func_wrapper(self.astng_from_file, fpath) - if astng is None or astng.name == base_name: + astroid = func_wrapper(self.ast_from_file, fpath) + if astroid is None or astroid.name == base_name: continue - project.add_module(astng) + project.add_module(astroid) return project - def register_transformer(self, transformer): - self.transformers.append(transformer) + def register_transform(self, node_class, transform, predicate=None): + """Register `transform(node)` function to be applied on the given + Astroid's `node_class` if `predicate` is None or return a true value + when called with the node as argument. + + The transform function may return a value which is then used to + substitute the original node in the tree. + """ + self.transforms.setdefault(node_class, []).append( (transform, predicate) ) class Project: """a project handle a set of modules / packages""" diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/mixins.py b/pylibs/pylama/checkers/pylint/astroid/mixins.py similarity index 91% rename from pylibs/pylama/checkers/pylint/logilab/astng/mixins.py rename to pylibs/pylama/checkers/pylint/astroid/mixins.py index 5d4a865c..42bbb404 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/mixins.py +++ b/pylibs/pylama/checkers/pylint/astroid/mixins.py @@ -1,24 +1,24 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """This module contains some mixins for the different nodes. """ -from .exceptions import (ASTNGBuildingException, InferenceError, +from .exceptions import (AstroidBuildingException, InferenceError, NotFoundError) @@ -101,7 +101,7 @@ def do_import_module(self, modname): return mymodule try: return mymodule.import_module(modname, level=level) - except ASTNGBuildingException: + except AstroidBuildingException: raise InferenceError(modname) except SyntaxError, ex: raise InferenceError(str(ex)) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/node_classes.py b/pylibs/pylama/checkers/pylint/astroid/node_classes.py similarity index 89% rename from pylibs/pylama/checkers/pylint/logilab/astng/node_classes.py rename to pylibs/pylama/checkers/pylint/astroid/node_classes.py index a7976774..52cd0a27 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/node_classes.py +++ b/pylibs/pylama/checkers/pylint/astroid/node_classes.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """Module for some node classes. More nodes in scoped_nodes.py """ @@ -253,9 +253,11 @@ class Name(LookupMixIn, NodeNG): class Arguments(NodeNG, AssignTypeMixin): """class representing an Arguments node""" - _astng_fields = ('args', 'defaults') + _astroid_fields = ('args', 'defaults', 'kwonlyargs', 'kw_defaults') args = None defaults = None + kwonlyargs = None + kw_defaults = None def __init__(self, vararg=None, kwarg=None): self.vararg = vararg @@ -268,11 +270,17 @@ def _infer_name(self, frame, name): def format_args(self): """return arguments formatted as string""" - result = [_format_args(self.args, self.defaults)] + result = [] + if self.args: + result.append(_format_args(self.args, self.defaults)) if self.vararg: result.append('*%s' % self.vararg) if self.kwarg: result.append('**%s' % self.kwarg) + if self.kwonlyargs: + if not self.vararg: + result.append('*') + result.append(_format_args(self.kwonlyargs, self.kw_defaults)) return ', '.join(result) def default_value(self, argname): @@ -285,6 +293,9 @@ def default_value(self, argname): idx = i - (len(self.args) - len(self.defaults)) if idx >= 0: return self.defaults[idx] + i = _find_arg(argname, self.kwonlyargs)[0] + if i is not None and self.kw_defaults[i] is not None: + return self.kw_defaults[i] raise NoDefault() def is_argument(self, name): @@ -301,6 +312,12 @@ def find_argname(self, argname, rec=False): return _find_arg(argname, self.args, rec) return None, None + def get_children(self): + """override get_children to skip over None elements in kw_defaults""" + for child in super(Arguments, self).get_children(): + if child is not None: + yield child + def _find_arg(argname, args, rec=False): for i, arg in enumerate(args): @@ -326,47 +343,48 @@ def _format_args(args, defaults=None): else: values.append(arg.name) if defaults is not None and i >= default_offset: - values[-1] += '=' + defaults[i-default_offset].as_string() + if defaults[i-default_offset] is not None: + values[-1] += '=' + defaults[i-default_offset].as_string() return ', '.join(values) class AssAttr(NodeNG, ParentAssignTypeMixin): """class representing an AssAttr node""" - _astng_fields = ('expr',) + _astroid_fields = ('expr',) expr = None class Assert(Statement): """class representing an Assert node""" - _astng_fields = ('test', 'fail',) + _astroid_fields = ('test', 'fail',) test = None fail = None class Assign(Statement, AssignTypeMixin): """class representing an Assign node""" - _astng_fields = ('targets', 'value',) + _astroid_fields = ('targets', 'value',) targets = None value = None class AugAssign(Statement, AssignTypeMixin): """class representing an AugAssign node""" - _astng_fields = ('target', 'value',) + _astroid_fields = ('target', 'value',) target = None value = None class Backquote(NodeNG): """class representing a Backquote node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class BinOp(NodeNG): """class representing a BinOp node""" - _astng_fields = ('left', 'right',) + _astroid_fields = ('left', 'right',) left = None right = None class BoolOp(NodeNG): """class representing a BoolOp node""" - _astng_fields = ('values',) + _astroid_fields = ('values',) values = None class Break(Statement): @@ -375,7 +393,7 @@ class Break(Statement): class CallFunc(NodeNG): """class representing a CallFunc node""" - _astng_fields = ('func', 'args', 'starargs', 'kwargs') + _astroid_fields = ('func', 'args', 'starargs', 'kwargs') func = None args = None starargs = None @@ -387,7 +405,7 @@ def __init__(self): class Compare(NodeNG): """class representing a Compare node""" - _astng_fields = ('left', 'ops',) + _astroid_fields = ('left', 'ops',) left = None ops = None @@ -405,7 +423,7 @@ def last_child(self): class Comprehension(NodeNG): """class representing a Comprehension node""" - _astng_fields = ('target', 'iter' ,'ifs') + _astroid_fields = ('target', 'iter' ,'ifs') target = None iter = None ifs = None @@ -458,7 +476,7 @@ class Continue(Statement): class Decorators(NodeNG): """class representing a Decorators node""" - _astng_fields = ('nodes',) + _astroid_fields = ('nodes',) nodes = None def __init__(self, nodes=None): @@ -470,19 +488,19 @@ def scope(self): class DelAttr(NodeNG, ParentAssignTypeMixin): """class representing a DelAttr node""" - _astng_fields = ('expr',) + _astroid_fields = ('expr',) expr = None class Delete(Statement, AssignTypeMixin): """class representing a Delete node""" - _astng_fields = ('targets',) + _astroid_fields = ('targets',) targets = None class Dict(NodeNG, Instance): """class representing a Dict node""" - _astng_fields = ('items',) + _astroid_fields = ('items',) def __init__(self, items=None): if items is None: @@ -524,7 +542,7 @@ def getitem(self, lookup_key, context=None): class Discard(Statement): """class representing a Discard node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None @@ -538,7 +556,7 @@ class EmptyNode(NodeNG): class ExceptHandler(Statement, AssignTypeMixin): """class representing an ExceptHandler node""" - _astng_fields = ('type', 'name', 'body',) + _astroid_fields = ('type', 'name', 'body',) type = None name = None body = None @@ -566,7 +584,7 @@ def catch(self, exceptions): class Exec(Statement): """class representing an Exec node""" - _astng_fields = ('expr', 'globals', 'locals',) + _astroid_fields = ('expr', 'globals', 'locals',) expr = None globals = None locals = None @@ -574,12 +592,12 @@ class Exec(Statement): class ExtSlice(NodeNG): """class representing an ExtSlice node""" - _astng_fields = ('dims',) + _astroid_fields = ('dims',) dims = None class For(BlockRangeMixIn, AssignTypeMixin, Statement): """class representing a For node""" - _astng_fields = ('target', 'iter', 'body', 'orelse',) + _astroid_fields = ('target', 'iter', 'body', 'orelse',) target = None iter = None body = None @@ -600,7 +618,7 @@ def __init__(self, fromname, names, level=0): class Getattr(NodeNG): """class representing a Getattr node""" - _astng_fields = ('expr',) + _astroid_fields = ('expr',) expr = None @@ -616,7 +634,7 @@ def _infer_name(self, frame, name): class If(BlockRangeMixIn, Statement): """class representing an If node""" - _astng_fields = ('test', 'body', 'orelse') + _astroid_fields = ('test', 'body', 'orelse') test = None body = None orelse = None @@ -636,7 +654,7 @@ def block_range(self, lineno): class IfExp(NodeNG): """class representing an IfExp node""" - _astng_fields = ('test', 'body', 'orelse') + _astroid_fields = ('test', 'body', 'orelse') test = None body = None orelse = None @@ -648,19 +666,19 @@ class Import(FromImportMixIn, Statement): class Index(NodeNG): """class representing an Index node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class Keyword(NodeNG): """class representing a Keyword node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class List(NodeNG, Instance, ParentAssignTypeMixin): """class representing a List node""" - _astng_fields = ('elts',) + _astroid_fields = ('elts',) def __init__(self, elts=None): if elts is None: @@ -694,7 +712,7 @@ class Pass(Statement): class Print(Statement): """class representing a Print node""" - _astng_fields = ('dest', 'values',) + _astroid_fields = ('dest', 'values',) dest = None values = None @@ -703,11 +721,11 @@ class Raise(Statement): """class representing a Raise node""" exc = None if sys.version_info < (3, 0): - _astng_fields = ('exc', 'inst', 'tback') + _astroid_fields = ('exc', 'inst', 'tback') inst = None tback = None else: - _astng_fields = ('exc', 'cause') + _astroid_fields = ('exc', 'cause') exc = None cause = None @@ -721,13 +739,13 @@ def raises_not_implemented(self): class Return(Statement): """class representing a Return node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class Set(NodeNG, Instance, ParentAssignTypeMixin): """class representing a Set node""" - _astng_fields = ('elts',) + _astroid_fields = ('elts',) def __init__(self, elts=None): if elts is None: @@ -744,27 +762,27 @@ def itered(self): class Slice(NodeNG): """class representing a Slice node""" - _astng_fields = ('lower', 'upper', 'step') + _astroid_fields = ('lower', 'upper', 'step') lower = None upper = None step = None class Starred(NodeNG, ParentAssignTypeMixin): """class representing a Starred node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class Subscript(NodeNG): """class representing a Subscript node""" - _astng_fields = ('value', 'slice') + _astroid_fields = ('value', 'slice') value = None slice = None class TryExcept(BlockRangeMixIn, Statement): """class representing a TryExcept node""" - _astng_fields = ('body', 'handlers', 'orelse',) + _astroid_fields = ('body', 'handlers', 'orelse',) body = None handlers = None orelse = None @@ -790,7 +808,7 @@ def block_range(self, lineno): class TryFinally(BlockRangeMixIn, Statement): """class representing a TryFinally node""" - _astng_fields = ('body', 'finalbody',) + _astroid_fields = ('body', 'finalbody',) body = None finalbody = None @@ -809,7 +827,7 @@ def block_range(self, lineno): class Tuple(NodeNG, Instance, ParentAssignTypeMixin): """class representing a Tuple node""" - _astng_fields = ('elts',) + _astroid_fields = ('elts',) def __init__(self, elts=None): if elts is None: @@ -829,13 +847,13 @@ def itered(self): class UnaryOp(NodeNG): """class representing an UnaryOp node""" - _astng_fields = ('operand',) + _astroid_fields = ('operand',) operand = None class While(BlockRangeMixIn, Statement): """class representing a While node""" - _astng_fields = ('test', 'body', 'orelse',) + _astroid_fields = ('test', 'body', 'orelse',) test = None body = None orelse = None @@ -850,21 +868,24 @@ def block_range(self, lineno): class With(BlockRangeMixIn, AssignTypeMixin, Statement): """class representing a With node""" - _astng_fields = ('expr', 'vars', 'body') - expr = None - vars = None + _astroid_fields = ('items', 'body') + items = None body = None def _blockstart_toline(self): - if self.vars: - return self.vars.tolineno - else: - return self.expr.tolineno + return self.items[-1][0].tolineno + def get_children(self): + for expr, var in self.items: + yield expr + if var: + yield var + for elt in self.body: + yield elt class Yield(NodeNG): """class representing a Yield node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None # constants ############################################################## @@ -889,7 +910,7 @@ def _update_const_classes(): _update_const_classes() def const_factory(value): - """return an astng node for a python value""" + """return an astroid node for a python value""" # XXX we should probably be stricter here and only consider stuff in # CONST_CLS or do better treatment: in case where value is not in CONST_CLS, # we should rather recall the builder on this value than returning an empty diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/nodes.py b/pylibs/pylama/checkers/pylint/astroid/nodes.py similarity index 88% rename from pylibs/pylama/checkers/pylint/logilab/astng/nodes.py rename to pylibs/pylama/checkers/pylint/astroid/nodes.py index b475b902..4deddde3 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/nodes.py +++ b/pylibs/pylama/checkers/pylint/astroid/nodes.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """ on all nodes : .is_statement, returning true if the node should be considered as a @@ -26,7 +26,7 @@ .frame(), returning the first node defining a new local scope (i.e. Module, Function or Class) .set_local(name, node), define an identifier on the first parent frame, - with the node defining it. This is used by the astng builder and should not + with the node defining it. This is used by the astroid builder and should not be used from out there. on From and Import : diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/protocols.py b/pylibs/pylama/checkers/pylint/astroid/protocols.py similarity index 96% rename from pylibs/pylama/checkers/pylint/logilab/astng/protocols.py rename to pylibs/pylama/checkers/pylint/astroid/protocols.py index 05feb246..109d1555 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/protocols.py +++ b/pylibs/pylama/checkers/pylint/astroid/protocols.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """this module contains a set of functions to handle python protocols for nodes where it makes sense. """ @@ -310,10 +310,11 @@ def excepthandler_assigned_stmts(self, node, context=None, asspath=None): def with_assigned_stmts(self, node, context=None, asspath=None): if asspath is None: - for lst in self.vars.infer(context): - if isinstance(lst, (nodes.Tuple, nodes.List)): - for item in lst.nodes: - yield item + for _, vars in self.items: + for lst in vars.infer(context): + if isinstance(lst, (nodes.Tuple, nodes.List)): + for item in lst.nodes: + yield item nodes.With.assigned_stmts = raise_if_nothing_infered(with_assigned_stmts) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/raw_building.py b/pylibs/pylama/checkers/pylint/astroid/raw_building.py similarity index 87% rename from pylibs/pylama/checkers/pylint/logilab/astng/raw_building.py rename to pylibs/pylama/checkers/pylint/astroid/raw_building.py index 076f28f3..b7144a86 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/raw_building.py +++ b/pylibs/pylama/checkers/pylint/astroid/raw_building.py @@ -1,21 +1,21 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""this module contains a set of functions to create astng trees from scratch +# with astroid. If not, see . +"""this module contains a set of functions to create astroid trees from scratch (build_* functions) or from living object (object_build_* functions) """ @@ -30,8 +30,8 @@ from .nodes import (Module, Class, Const, const_factory, From, Function, EmptyNode, Name, Arguments) from .bases import BUILTINS, Generator -from .manager import ASTNGManager -MANAGER = ASTNGManager() +from .manager import AstroidManager +MANAGER = AstroidManager() _CONSTANTS = tuple(CONST_CLS) # the keys of CONST_CLS eg python builtin types @@ -67,14 +67,14 @@ def attach_import_node(node, modname, membername): def build_module(name, doc=None): - """create and initialize a astng Module node""" + """create and initialize a astroid Module node""" node = Module(name, doc, pure_python=False) node.package = False node.parent = None return node def build_class(name, basenames=(), doc=None): - """create and initialize a astng Class node""" + """create and initialize a astroid Class node""" node = Class(name, doc) for base in basenames: basenode = Name() @@ -84,7 +84,7 @@ def build_class(name, basenames=(), doc=None): return node def build_function(name, args=None, defaults=None, flag=0, doc=None): - """create and initialize a astng Function node""" + """create and initialize a astroid Function node""" args, defaults = args or [], defaults or [] # first argument is now a list of decorators func = Function(name, doc) @@ -107,7 +107,7 @@ def build_function(name, args=None, defaults=None, flag=0, doc=None): def build_from_import(fromname, names): - """create and initialize an astng From import statement""" + """create and initialize an astroid From import statement""" return From(fromname, [(name, None) for name in names]) def register_arguments(func, args=None): @@ -129,13 +129,13 @@ def register_arguments(func, args=None): register_arguments(func, arg.elts) def object_build_class(node, member, localname): - """create astng for a living class object""" + """create astroid for a living class object""" basenames = [base.__name__ for base in member.__bases__] return _base_class_object_build(node, member, basenames, localname=localname) def object_build_function(node, member, localname): - """create astng for a living function object""" + """create astroid for a living function object""" args, varargs, varkw, defaults = getargspec(member) if varargs is not None: args.append(varargs) @@ -146,11 +146,11 @@ def object_build_function(node, member, localname): node.add_local_node(func, localname) def object_build_datadescriptor(node, member, name): - """create astng for a living data descriptor object""" + """create astroid for a living data descriptor object""" return _base_class_object_build(node, member, [], name) def object_build_methoddescriptor(node, member, localname): - """create astng for a living method descriptor object""" + """create astroid for a living method descriptor object""" # FIXME get arguments ? func = build_function(getattr(member, '__name__', None) or localname, doc=member.__doc__) @@ -160,7 +160,7 @@ def object_build_methoddescriptor(node, member, localname): node.add_local_node(func, localname) def _base_class_object_build(node, member, basenames, name=None, localname=None): - """create astng for a living class object, with a given set of base names + """create astroid for a living class object, with a given set of base names (e.g. ancestors) """ klass = build_class(name or getattr(member, '__name__', None) or localname, @@ -197,14 +197,14 @@ class InspectBuilder(object): Function and Class nodes and some others as guessed. """ - # astng from living objects ############################################### + # astroid from living objects ############################################### def __init__(self): self._done = {} self._module = None def inspect_build(self, module, modname=None, path=None): - """build astng from a living module (i.e. using inspect) + """build astroid from a living module (i.e. using inspect) this is used when there is no python source code available (either because it's a built-in module or because the .py is not available) """ @@ -217,7 +217,7 @@ def inspect_build(self, module, modname=None, path=None): # in jython, java modules have no __doc__ (see #109562) node = build_module(modname) node.file = node.path = path and abspath(path) or path - MANAGER.astng_cache[modname] = node + MANAGER.astroid_cache[modname] = node node.package = hasattr(module, '__path__') self._done = {} self.object_build(node, module) @@ -289,7 +289,7 @@ def imported_member(self, node, member, name): modname = getattr(member, '__module__', None) except: # XXX use logging - print 'unexpected error while building astng from living object' + print 'unexpected error while building astroid from living object' import traceback traceback.print_exc() modname = None @@ -315,28 +315,28 @@ def imported_member(self, node, member, name): return False -### astng boot strapping ################################################### ### -ASTNG_BUILDER = InspectBuilder() +### astroid boot strapping ################################################### ### +Astroid_BUILDER = InspectBuilder() _CONST_PROXY = {} -def astng_boot_strapping(): - """astng boot strapping the builtins module""" +def astroid_boot_strapping(): + """astroid boot strapping the builtins module""" # this boot strapping is necessary since we need the Const nodes to # inspect_build builtins, and then we can proxy Const - from ..common.compat import builtins - astng_builtin = ASTNG_BUILDER.inspect_build(builtins) + from ..logilab.common.compat import builtins + astroid_builtin = Astroid_BUILDER.inspect_build(builtins) for cls, node_cls in CONST_CLS.items(): if cls is type(None): proxy = build_class('NoneType') - proxy.parent = astng_builtin + proxy.parent = astroid_builtin else: - proxy = astng_builtin.getattr(cls.__name__)[0] + proxy = astroid_builtin.getattr(cls.__name__)[0] if cls in (dict, list, set, tuple): node_cls._proxied = proxy else: _CONST_PROXY[cls] = proxy -astng_boot_strapping() +astroid_boot_strapping() # TODO : find a nicer way to handle this situation; # However __proxied introduced an @@ -347,5 +347,5 @@ def _set_proxied(const): from types import GeneratorType Generator._proxied = Class(GeneratorType.__name__, GeneratorType.__doc__) -ASTNG_BUILDER.object_build(Generator._proxied, GeneratorType) +Astroid_BUILDER.object_build(Generator._proxied, GeneratorType) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/rebuilder.py b/pylibs/pylama/checkers/pylint/astroid/rebuilder.py similarity index 90% rename from pylibs/pylama/checkers/pylint/logilab/astng/rebuilder.py rename to pylibs/pylama/checkers/pylint/astroid/rebuilder.py index 03ee6128..d0f6bde2 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/rebuilder.py +++ b/pylibs/pylama/checkers/pylint/astroid/rebuilder.py @@ -1,25 +1,26 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """this module contains utilities for rebuilding a _ast tree in -order to get a single ASTNG representation +order to get a single Astroid representation """ import sys +from warnings import warn from _ast import (Expr as Discard, Str, # binary operators Add, Div, FloorDiv, Mod, Mult, Pow, Sub, BitAnd, BitOr, BitXor, @@ -117,29 +118,56 @@ def _set_infos(oldnode, newnode, parent): class TreeRebuilder(object): - """Rebuilds the _ast tree to become an ASTNG tree""" + """Rebuilds the _ast tree to become an Astroid tree""" - _visit_meths = {} - def __init__(self): - self.init() - - def init(self): + def __init__(self, manager): + self._manager = manager self.asscontext = None self._metaclass = [''] self._global_names = [] self._from_nodes = [] self._delayed_assattr = [] + self._visit_meths = {} + + def _transform(self, node): + try: + transforms = self._manager.transforms[type(node)] + except KeyError: + return node # no transform registered for this class of node + orig_node = node # copy the reference + for transform_func, predicate in transforms: + if predicate is None or predicate(node): + ret = transform_func(node) + # if the transformation function returns something, it's + # expected to be a replacement for the node + if ret is not None: + if node is not orig_node: + # node has already be modified by some previous + # transformation, warn about it + warn('node %s substitued multiple times' % node) + node = ret + return node + + def visit_module(self, node, modname, package): + """visit a Module node by returning a fresh instance of it""" + newnode = new.Module(modname, None) + newnode.package = package + _lineno_parent(node, newnode, parent=None) + _init_set_doc(node, newnode) + newnode.body = [self.visit(child, newnode) for child in node.body] + newnode.set_line_info(newnode.last_child()) + return self._transform(newnode) def visit(self, node, parent): cls = node.__class__ if cls in self._visit_meths: - return self._visit_meths[cls](node, parent) + visit_method = self._visit_meths[cls] else: cls_name = cls.__name__ visit_name = 'visit_' + REDIRECT.get(cls_name, cls_name).lower() visit_method = getattr(self, visit_name) self._visit_meths[cls] = visit_method - return visit_method(node, parent) + return self._transform(visit_method(node, parent)) def _save_assignment(self, node, name=None): """save assignement situation since node.parent is not available yet""" @@ -157,6 +185,8 @@ def visit_arguments(self, node, parent): newnode.args = [self.visit(child, newnode) for child in node.args] self.asscontext = None newnode.defaults = [self.visit(child, newnode) for child in node.defaults] + newnode.kwonlyargs = [] + newnode.kw_defaults = [] newnode.vararg = node.vararg newnode.kwarg = node.kwarg # save argument names in locals: @@ -287,7 +317,7 @@ def visit_callfunc(self, node, parent): return newnode def visit_class(self, node, parent): - """visit a Class node to become astng""" + """visit a Class node to become astroid""" self._metaclass.append(self._metaclass[-1]) newnode = new.Class(node.name, None) _lineno_parent(node, newnode, parent) @@ -342,7 +372,7 @@ def visit_comprehension(self, node, parent): def visit_decorators(self, node, parent): """visit a Decorators node by returning a fresh instance of it""" # /!\ node is actually a _ast.Function node while - # parent is a astng.nodes.Function node + # parent is a astroid.nodes.Function node newnode = new.Decorators() _lineno_parent(node, newnode, parent) if 'decorators' in node._fields: # py < 2.6, i.e. 2.5 @@ -461,7 +491,7 @@ def visit_from(self, node, parent): return newnode def visit_function(self, node, parent): - """visit an Function node to become astng""" + """visit an Function node to become astroid""" self._global_names.append({}) newnode = new.Function(node.name, None) _lineno_parent(node, newnode, parent) @@ -523,7 +553,7 @@ def visit_getattr(self, node, parent): return newnode def visit_global(self, node, parent): - """visit an Global node to become astng""" + """visit an Global node to become astroid""" newnode = new.Global(node.names) _set_infos(node, newnode, parent) if self._global_names: # global at the module level, no effect @@ -606,16 +636,6 @@ def visit_listcomp(self, node, parent): newnode.set_line_info(newnode.last_child()) return newnode - def visit_module(self, node, modname, package): - """visit a Module node by returning a fresh instance of it""" - newnode = new.Module(modname, None) - newnode.package = package - _lineno_parent(node, newnode, parent=None) - _init_set_doc(node, newnode) - newnode.body = [self.visit(child, newnode) for child in node.body] - newnode.set_line_info(newnode.last_child()) - return newnode - def visit_name(self, node, parent): """visit a Name node by returning a fresh instance of it""" # True and False can be assigned to something in py2x, so we have to @@ -787,12 +807,14 @@ def visit_while(self, node, parent): def visit_with(self, node, parent): newnode = new.With() _lineno_parent(node, newnode, parent) - _node = getattr(node, 'items', [node])[0] # python 3.3 XXX - newnode.expr = self.visit(_node.context_expr, newnode) + expr = self.visit(node.context_expr, newnode) self.asscontext = "Ass" - if _node.optional_vars is not None: - newnode.vars = self.visit(_node.optional_vars, newnode) + if node.optional_vars is not None: + vars = self.visit(node.optional_vars, newnode) + else: + vars = None self.asscontext = None + newnode.items = [(expr, vars)] newnode.body = [self.visit(child, newnode) for child in node.body] newnode.set_line_info(newnode.last_child()) return newnode @@ -813,9 +835,17 @@ class TreeRebuilder3k(TreeRebuilder): def visit_arg(self, node, parent): """visit a arg node by returning a fresh AssName instance""" # the node is coming from py>=3.0, but we use AssName in py2.x - # XXX or we should instead introduce a Arg node in astng ? + # XXX or we should instead introduce a Arg node in astroid ? return self.visit_assname(node, parent, node.arg) + def visit_arguments(self, node, parent): + newnode = super(TreeRebuilder3k, self).visit_arguments(node, parent) + self.asscontext = "Ass" + newnode.kwonlyargs = [self.visit(child, newnode) for child in node.kwonlyargs] + self.asscontext = None + newnode.kw_defaults = [self.visit(child, newnode) if child else None for child in node.kw_defaults] + return newnode + def visit_excepthandler(self, node, parent): """visit an ExceptHandler node by returning a fresh instance of it""" newnode = new.ExceptHandler() @@ -862,10 +892,11 @@ def visit_try(self, node, parent): newnode.finalbody = [self.visit(n, newnode) for n in node.finalbody] if node.handlers: excnode = new.TryExcept() - _lineno_parent(node, excnode, parent) - excnode.body = [self.visit(child, newnode) for child in node.body] - excnode.handlers = [self.visit(child, newnode) for child in node.handlers] - excnode.orelse = [self.visit(child, newnode) for child in node.orelse] + _lineno_parent(node, excnode, newnode) + excnode.body = [self.visit(child, excnode) for child in node.body] + excnode.handlers = [self.visit(child, excnode) for child in node.handlers] + excnode.orelse = [self.visit(child, excnode) for child in node.orelse] + excnode.set_line_info(excnode.last_child()) newnode.body = [excnode] else: newnode.body = [self.visit(child, newnode) for child in node.body] @@ -878,6 +909,28 @@ def visit_try(self, node, parent): newnode.set_line_info(newnode.last_child()) return newnode + def visit_with(self, node, parent): + if 'items' not in node._fields: + # python < 3.3 + return super(TreeRebuilder3k, self).visit_with(node, parent) + + newnode = new.With() + _lineno_parent(node, newnode, parent) + def visit_child(child): + expr = self.visit(child.context_expr, newnode) + self.asscontext = 'Ass' + if child.optional_vars: + var = self.visit(child.optional_vars, newnode) + else: + var = None + self.asscontext = None + return expr, var + newnode.items = [visit_child(child) + for child in node.items] + newnode.body = [self.visit(child, newnode) for child in node.body] + newnode.set_line_info(newnode.last_child()) + return newnode + def visit_yieldfrom(self, node, parent): return self.visit_yield(node, parent) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/scoped_nodes.py b/pylibs/pylama/checkers/pylint/astroid/scoped_nodes.py similarity index 91% rename from pylibs/pylama/checkers/pylint/logilab/astng/scoped_nodes.py rename to pylibs/pylama/checkers/pylint/astroid/scoped_nodes.py index 8e049775..e52c89f7 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/scoped_nodes.py +++ b/pylibs/pylama/checkers/pylint/astroid/scoped_nodes.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """This module contains the classes for "scoped" node, i.e. which are opening a new local scope in the language definition : Module, Class, Function (and Lambda, GenExpr, DictComp and SetComp to some extent). @@ -26,11 +26,11 @@ import sys from itertools import chain -from ..common.compat import builtins -from ..common.decorators import cached +from ..logilab.common.compat import builtins +from ..logilab.common.decorators import cached from .exceptions import NotFoundError, \ - ASTNGBuildingException, InferenceError + AstroidBuildingException, InferenceError from .node_classes import Const, DelName, DelAttr, \ Dict, From, List, Pass, Raise, Return, Tuple, Yield, \ LookupMixIn, const_factory as cf, unpack_infer @@ -39,7 +39,7 @@ BUILTINS from .mixins import FilterStmtsMixin from .bases import Statement -from .manager import ASTNGManager +from .manager import AstroidManager def remove_nodes(func, cls): @@ -72,20 +72,20 @@ def std_special_attributes(self, name, add_locals=True): return [Dict()] + locals.get(name, []) raise NotFoundError(name) -MANAGER = ASTNGManager() +MANAGER = AstroidManager() def builtin_lookup(name): """lookup a name into the builtin module - return the list of matching statements and the astng for the builtin + return the list of matching statements and the astroid for the builtin module """ - builtin_astng = MANAGER.astng_from_module(builtins) + builtin_astroid = MANAGER.ast_from_module(builtins) if name == '__dict__': - return builtin_astng, () + return builtin_astroid, () try: - stmts = builtin_astng.locals[name] + stmts = builtin_astroid.locals[name] except KeyError: stmts = () - return builtin_astng, stmts + return builtin_astroid, stmts # TODO move this Mixin to mixins.py; problem: 'Function' in _scope_lookup @@ -207,14 +207,14 @@ def __contains__(self, name): # Module ##################################################################### class Module(LocalsDictNodeNG): - _astng_fields = ('body',) + _astroid_fields = ('body',) fromlineno = 0 lineno = 0 # attributes below are set by the builder module or by raw factories - # the file from which as been extracted the astng representation. It may + # the file from which as been extracted the astroid representation. It may # be None if the representation has been built from a built-in module file = None # encoding of python source file, so we can get unicode out of it (python2 @@ -222,7 +222,7 @@ class Module(LocalsDictNodeNG): file_encoding = None # the module name name = None - # boolean for astng built from source (i.e. ast) + # boolean for astroid built from source (i.e. ast) pure_python = None # boolean for package module package = None @@ -246,7 +246,7 @@ def __init__(self, name, doc, pure_python=True): @property def file_stream(self): if self.file is not None: - return open(self.file) + return open(self.file, 'rb') return None def block_range(self, lineno): @@ -282,7 +282,7 @@ def getattr(self, name, context=None, ignore_locals=False): if self.package: try: return [self.import_module(name, relative_only=True)] - except ASTNGBuildingException: + except AstroidBuildingException: raise NotFoundError(name) except Exception:# XXX pylint tests never pass here; do we need it? import traceback @@ -336,13 +336,13 @@ def import_module(self, modname, relative_only=False, level=None): level = 0 absmodname = self.relative_to_absolute_name(modname, level) try: - return MANAGER.astng_from_module_name(absmodname) - except ASTNGBuildingException: + return MANAGER.ast_from_module_name(absmodname) + except AstroidBuildingException: # we only want to import a sub module or package of this module, # skip here if relative_only: raise - return MANAGER.astng_from_module_name(modname) + return MANAGER.ast_from_module_name(modname) def relative_to_absolute_name(self, modname, level): """return the absolute module name for a relative import. @@ -350,7 +350,7 @@ def relative_to_absolute_name(self, modname, level): The relative import can be implicit or explicit. """ # XXX this returns non sens when called on an absolute import - # like 'pylint.checkers.logilab.astng.utils' + # like 'pylint.checkers.astroid.utils' # XXX doesn't return absolute name if self.name isn't absolute name if self.absolute_import_activated() and level is None: return modname @@ -387,7 +387,7 @@ def wildcard_import_names(self): except AttributeError: return [name for name in living.__dict__.keys() if not name.startswith('_')] - # else lookup the astng + # else lookup the astroid # # We separate the different steps of lookup in try/excepts # to avoid catching too many Exceptions @@ -419,7 +419,7 @@ def frame(self): class GenExpr(ComprehensionScope): - _astng_fields = ('elt', 'generators') + _astroid_fields = ('elt', 'generators') def __init__(self): self.locals = {} @@ -428,7 +428,7 @@ def __init__(self): class DictComp(ComprehensionScope): - _astng_fields = ('key', 'value', 'generators') + _astroid_fields = ('key', 'value', 'generators') def __init__(self): self.locals = {} @@ -438,7 +438,7 @@ def __init__(self): class SetComp(ComprehensionScope): - _astng_fields = ('elt', 'generators') + _astroid_fields = ('elt', 'generators') def __init__(self): self.locals = {} @@ -448,7 +448,7 @@ def __init__(self): class _ListComp(NodeNG): """class representing a ListComp node""" - _astng_fields = ('elt', 'generators') + _astroid_fields = ('elt', 'generators') elt = None generators = None @@ -465,7 +465,7 @@ class ListComp(_ListComp): class Lambda(LocalsDictNodeNG, FilterStmtsMixin): - _astng_fields = ('args', 'body',) + _astroid_fields = ('args', 'body',) name = '' # function's type, 'function' | 'method' | 'staticmethod' | 'classmethod' @@ -506,7 +506,7 @@ def infer_call_result(self, caller, context=None): return self.body.infer(context) def scope_lookup(self, node, name, offset=0): - if node in self.args.defaults: + if node in self.args.defaults or node in self.args.kw_defaults: frame = self.parent.frame() # line offset to avoid that def func(f=func) resolve the default # value to the defined function @@ -518,7 +518,7 @@ def scope_lookup(self, node, name, offset=0): class Function(Statement, Lambda): - _astng_fields = ('decorators', 'args', 'body') + _astroid_fields = ('decorators', 'args', 'body') special_attributes = set(('__name__', '__doc__', '__dict__')) is_function = True @@ -542,6 +542,8 @@ def set_line_info(self, lastchild): if self.decorators is not None: self.fromlineno += sum(node.tolineno - node.lineno + 1 for node in self.decorators.nodes) + if self.args.fromlineno < self.fromlineno: + self.args.fromlineno = self.fromlineno self.tolineno = lastchild.tolineno self.blockstart_tolineno = self.args.tolineno @@ -586,10 +588,23 @@ def is_bound(self): return self.type == 'classmethod' def is_abstract(self, pass_is_abstract=True): - """return true if the method is abstract - It's considered as abstract if the only statement is a raise of - NotImplementError, or, if pass_is_abstract, a pass statement + """Returns True if the method is abstract. + + A method is considered abstract if + - the only statement is 'raise NotImplementedError', or + - the only statement is 'pass' and pass_is_abstract is True, or + - the method is annotated with abc.astractproperty/abc.abstractmethod """ + if self.decorators: + for node in self.decorators.nodes: + try: + infered = node.infer().next() + except InferenceError: + continue + if infered and infered.qname() in ('abc.abstractproperty', + 'abc.abstractmethod'): + return True + for child_node in self.body: if isinstance(child_node, Raise): if child_node.raises_not_implemented(): @@ -683,7 +698,7 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin): # by a raw factories # a dictionary of class instances attributes - _astng_fields = ('decorators', 'bases', 'body') # name + _astroid_fields = ('decorators', 'bases', 'body') # name decorators = None special_attributes = set(('__name__', '__doc__', '__dict__', '__module__', @@ -802,20 +817,20 @@ def ancestors(self, recurs=True, context=None): continue def local_attr_ancestors(self, name, context=None): - """return an iterator on astng representation of parent classes + """return an iterator on astroid representation of parent classes which have defined in their locals """ - for astng in self.ancestors(context=context): - if name in astng: - yield astng + for astroid in self.ancestors(context=context): + if name in astroid: + yield astroid def instance_attr_ancestors(self, name, context=None): - """return an iterator on astng representation of parent classes + """return an iterator on astroid representation of parent classes which have defined in their instance attribute dictionary """ - for astng in self.ancestors(context=context): - if name in astng.instance_attrs: - yield astng + for astroid in self.ancestors(context=context): + if name in astroid.instance_attrs: + yield astroid def has_base(self, node): return node in self.bases @@ -838,7 +853,7 @@ def local_attr(self, name, context=None): local_attr = remove_nodes(local_attr, DelAttr) def instance_attr(self, name, context=None): - """return the astng nodes associated to name in this class instance + """return the astroid nodes associated to name in this class instance attributes dictionary and in its parents :raises `NotFoundError`: @@ -940,8 +955,8 @@ def methods(self): its ancestors """ done = {} - for astng in chain(iter((self,)), self.ancestors()): - for meth in astng.mymethods(): + for astroid in chain(iter((self,)), self.ancestors()): + for meth in astroid.mymethods(): if meth.name in done: continue done[meth.name] = None diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/utils.py b/pylibs/pylama/checkers/pylint/astroid/utils.py similarity index 90% rename from pylibs/pylama/checkers/pylint/logilab/astng/utils.py rename to pylibs/pylama/checkers/pylint/astroid/utils.py index 325535b0..322e25b2 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/utils.py +++ b/pylibs/pylama/checkers/pylint/astroid/utils.py @@ -1,27 +1,27 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """this module contains some utilities to navigate in the tree or to extract information from it """ __docformat__ = "restructuredtext en" -from .exceptions import ASTNGBuildingException +from .exceptions import AstroidBuildingException from .builder import parse @@ -123,12 +123,12 @@ def _check_children(node): if not ok: print "lines;", node.lineno, child.lineno print "of module", node.root(), node.root().name - raise ASTNGBuildingException + raise AstroidBuildingException _check_children(child) class TreeTester(object): - '''A helper class to see _ast tree and compare with astng tree + '''A helper class to see _ast tree and compare with astroid tree indent: string for tree indent representation lineno: bool to tell if we should print the line numbers @@ -141,7 +141,7 @@ class TreeTester(object): . . . nl = True . ] - >>> print tester.astng_tree_repr() + >>> print tester.astroid_tree_repr() Module() body = [ Print() @@ -219,16 +219,16 @@ def _native_repr_tree(self, node, indent, _done=None): self._string += '\n' + indent + field + " = " + repr(attr) - def build_astng_tree(self): - """build astng tree from the _ast tree + def build_astroid_tree(self): + """build astroid tree from the _ast tree """ - from logilab.astng.builder import ASTNGBuilder - tree = ASTNGBuilder().string_build(self.sourcecode) + from .builder import AstroidBuilder + tree = AstroidBuilder().string_build(self.sourcecode) return tree - def astng_tree_repr(self, ids=False): - """build the astng tree and return a nice tree representation""" - mod = self.build_astng_tree() + def astroid_tree_repr(self, ids=False): + """build the astroid tree and return a nice tree representation""" + mod = self.build_astroid_tree() return mod.repr_tree(ids) diff --git a/pylibs/pylama/checkers/pylint/checkers/__init__.py b/pylibs/pylama/checkers/pylint/checkers/__init__.py index bc722279..dddc1bab 100644 --- a/pylibs/pylama/checkers/pylint/checkers/__init__.py +++ b/pylibs/pylama/checkers/pylint/checkers/__init__.py @@ -38,15 +38,16 @@ """ +import sys import tokenize -from os import listdir -from os.path import dirname, join, isdir, splitext +import warnings +from os.path import dirname -from ..logilab.astng.utils import ASTWalker -from ..logilab.common.modutils import load_module_from_file +from ..astroid.utils import ASTWalker from ..logilab.common.configuration import OptionsProviderMixIn -from ..reporters import diff_string, EmptyReport +from ..reporters import diff_string +from ..utils import register_plugins def table_lines_from_stats(stats, old_stats, columns): """get values listed in from and , @@ -121,43 +122,31 @@ def process_module(self, node): stream must implement the readline method """ + warnings.warn("Modules that need access to the tokens should " + "use the ITokenChecker interface.", + DeprecationWarning) stream = node.file_stream - stream.seek(0) # XXX may be removed with astng > 0.23 - self.process_tokens(tokenize.generate_tokens(stream.readline)) + stream.seek(0) # XXX may be removed with astroid > 0.23 + if sys.version_info <= (3, 0): + self.process_tokens(tokenize.generate_tokens(stream.readline)) + else: + self.process_tokens(tokenize.tokenize(stream.readline)) def process_tokens(self, tokens): """should be overridden by subclasses""" raise NotImplementedError() -PY_EXTS = ('.py', '.pyc', '.pyo', '.pyw', '.so', '.dll') +class BaseTokenChecker(BaseChecker): + """Base class for checkers that want to have access to the token stream.""" + + def process_tokens(self, tokens): + """Should be overridden by subclasses.""" + raise NotImplementedError() + def initialize(linter): """initialize linter with checkers in this package """ - package_load(linter, __path__[0]) + register_plugins(linter, __path__[0]) -def package_load(linter, directory): - """load all module and package in the given directory, looking for a - 'register' function in each one, used to register pylint checkers - """ - imported = {} - for filename in listdir(directory): - basename, extension = splitext(filename) - if basename in imported or basename == '__pycache__': - continue - if extension in PY_EXTS and basename != '__init__' or ( - not extension and isdir(join(directory, basename))): - try: - module = load_module_from_file(join(directory, filename)) - except ValueError: - # empty module name (usually emacs auto-save files) - continue - except ImportError, exc: - import sys - print >> sys.stderr, "Problem importing module %s: %s" % (filename, exc) - else: - if hasattr(module, 'register'): - module.register(linter) - imported[basename] = 1 - -__all__ = ('BaseChecker', 'initialize', 'package_load') +__all__ = ('BaseChecker', 'initialize') diff --git a/pylibs/pylama/checkers/pylint/checkers/base.py b/pylibs/pylama/checkers/pylint/checkers/base.py index 495076f8..5282898e 100644 --- a/pylibs/pylama/checkers/pylint/checkers/base.py +++ b/pylibs/pylama/checkers/pylint/checkers/base.py @@ -1,6 +1,7 @@ # Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). -# Copyright (c) 2009-2010 Arista Networks, Inc. # http://www.logilab.fr/ -- mailto:contact@logilab.fr +# Copyright (c) 2009-2010 Arista Networks, Inc. +# # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -15,30 +16,35 @@ # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """basic checker for Python code""" - -from ..logilab import astng +import sys +from .. import astroid from ..logilab.common.ureports import Table -from ..logilab.astng import are_exclusive +from ..astroid import are_exclusive, bases -from ..interfaces import IASTNGChecker +from ..interfaces import IAstroidChecker +from ..utils import EmptyReport from ..reporters import diff_string -from ..checkers import BaseChecker, EmptyReport -from ..checkers.utils import ( +from . import BaseChecker +from .utils import ( check_messages, clobber_in_except, + is_builtin_object, is_inside_except, + overrides_a_method, safe_infer, ) import re + # regex for class/function/variable/constant name CLASS_NAME_RGX = re.compile('[A-Z_][a-zA-Z0-9]+$') MOD_NAME_RGX = re.compile('(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$') CONST_NAME_RGX = re.compile('(([A-Z_][A-Z0-9_]*)|(__.*__))$') COMP_VAR_RGX = re.compile('[A-Za-z_][A-Za-z0-9_]*$') DEFAULT_NAME_RGX = re.compile('[a-z_][a-z0-9_]{2,30}$') +CLASS_ATTRIBUTE_RGX = re.compile(r'([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$') # do not require a doc string on system methods NO_REQUIRED_DOC_RGX = re.compile('__.*__') @@ -48,8 +54,8 @@ def in_loop(node): """return True if the node is inside a kind of for loop""" parent = node.parent while parent is not None: - if isinstance(parent, (astng.For, astng.ListComp, astng.SetComp, - astng.DictComp, astng.GenExpr)): + if isinstance(parent, (astroid.For, astroid.ListComp, astroid.SetComp, + astroid.DictComp, astroid.GenExpr)): return True parent = parent.parent return False @@ -68,17 +74,48 @@ def in_nested_list(nested_list, obj): def _loop_exits_early(loop): """Returns true if a loop has a break statement in its body.""" - loop_nodes = (astng.For, astng.While) + loop_nodes = (astroid.For, astroid.While) # Loop over body explicitly to avoid matching break statements # in orelse. for child in loop.body: if isinstance(child, loop_nodes): continue - for _ in child.nodes_of_class(astng.Break, skip_klass=loop_nodes): + for _ in child.nodes_of_class(astroid.Break, skip_klass=loop_nodes): return True return False + +def _determine_function_name_type(node): + """Determine the name type whose regex the a function's name should match. + + :param node: A function node. + :returns: One of ('function', 'method', 'attr') + """ + if not node.is_method(): + return 'function' + if node.decorators: + decorators = node.decorators.nodes + else: + decorators = [] + for decorator in decorators: + # If the function is a property (decorated with @property + # or @abc.abstractproperty), the name type is 'attr'. + if (isinstance(decorator, astroid.Name) or + (isinstance(decorator, astroid.Getattr) and + decorator.attrname == 'abstractproperty')): + infered = safe_infer(decorator) + if (infered and + infered.qname() in ('__builtin__.property', 'abc.abstractproperty')): + return 'attr' + # If the function is decorated using the prop_method.{setter,getter} + # form, treat it like an attribute as well. + elif (isinstance(decorator, astroid.Getattr) and + decorator.attrname in ('setter', 'deleter')): + return 'attr' + return 'method' + + def report_by_type_stats(sect, stats, old_stats): """make a report of @@ -130,13 +167,13 @@ def x(self, value): self._x = value """ if node.decorators: for decorator in node.decorators.nodes: - if (isinstance(decorator, astng.Getattr) and + if (isinstance(decorator, astroid.Getattr) and getattr(decorator.expr, 'name', None) == node.name): return True return False class _BasicChecker(BaseChecker): - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'basic' class BasicErrorChecker(_BasicChecker): @@ -187,79 +224,85 @@ class BasicErrorChecker(_BasicChecker): def __init__(self, linter): _BasicChecker.__init__(self, linter) - @check_messages('E0102') + @check_messages('function-redefined') def visit_class(self, node): self._check_redefinition('class', node) - @check_messages('E0100', 'E0101', 'E0102', 'E0106', 'E0108') + @check_messages('init-is-generator', 'return-in-init', + 'function-redefined', 'return-arg-in-generator', + 'duplicate-argument-name') def visit_function(self, node): if not redefined_by_decorator(node): self._check_redefinition(node.is_method() and 'method' or 'function', node) # checks for max returns, branch, return in __init__ - returns = node.nodes_of_class(astng.Return, - skip_klass=(astng.Function, astng.Class)) + returns = node.nodes_of_class(astroid.Return, + skip_klass=(astroid.Function, astroid.Class)) if node.is_method() and node.name == '__init__': if node.is_generator(): - self.add_message('E0100', node=node) + self.add_message('init-is-generator', node=node) else: values = [r.value for r in returns] - if [v for v in values if not (v is None or - (isinstance(v, astng.Const) and v.value is None) - or (isinstance(v, astng.Name) and v.name == 'None'))]: - self.add_message('E0101', node=node) + # Are we returning anything but None from constructors + if [v for v in values if + not (v is None or + (isinstance(v, astroid.Const) and v.value is None) or + (isinstance(v, astroid.Name) and v.name == 'None') + ) ]: + self.add_message('return-in-init', node=node) elif node.is_generator(): # make sure we don't mix non-None returns and yields for retnode in returns: - if isinstance(retnode.value, astng.Const) and \ + if isinstance(retnode.value, astroid.Const) and \ retnode.value.value is not None: - self.add_message('E0106', node=node, + self.add_message('return-arg-in-generator', node=node, line=retnode.fromlineno) + # Check for duplicate names args = set() for name in node.argnames(): if name in args: - self.add_message('E0108', node=node, args=(name,)) + self.add_message('duplicate-argument-name', node=node, args=(name,)) else: args.add(name) - @check_messages('E0104') + @check_messages('return-outside-function') def visit_return(self, node): - if not isinstance(node.frame(), astng.Function): - self.add_message('E0104', node=node) + if not isinstance(node.frame(), astroid.Function): + self.add_message('return-outside-function', node=node) - @check_messages('E0105') + @check_messages('yield-outside-function') def visit_yield(self, node): - if not isinstance(node.frame(), (astng.Function, astng.Lambda)): - self.add_message('E0105', node=node) + if not isinstance(node.frame(), (astroid.Function, astroid.Lambda)): + self.add_message('yield-outside-function', node=node) - @check_messages('E0103') + @check_messages('not-in-loop') def visit_continue(self, node): self._check_in_loop(node, 'continue') - @check_messages('E0103') + @check_messages('not-in-loop') def visit_break(self, node): self._check_in_loop(node, 'break') - @check_messages('W0120') + @check_messages('useless-else-on-loop') def visit_for(self, node): self._check_else_on_loop(node) - @check_messages('W0120') + @check_messages('useless-else-on-loop') def visit_while(self, node): self._check_else_on_loop(node) - @check_messages('E0107') + @check_messages('nonexistent-operator') def visit_unaryop(self, node): - """check use of the non-existent ++ adn -- operator operator""" + """check use of the non-existent ++ and -- operator operator""" if ((node.op in '+-') and - isinstance(node.operand, astng.UnaryOp) and + isinstance(node.operand, astroid.UnaryOp) and (node.operand.op == node.op)): - self.add_message('E0107', node=node, args=node.op*2) + self.add_message('nonexistent-operator', node=node, args=node.op*2) def _check_else_on_loop(self, node): """Check that any loop with an else clause has a break statement.""" if node.orelse and not _loop_exits_early(node): - self.add_message('W0120', node=node, + self.add_message('useless-else-on-loop', node=node, # This is not optimal, but the line previous # to the first statement in the else clause # will usually be the one that contains the else:. @@ -269,17 +312,17 @@ def _check_in_loop(self, node, node_name): """check that a node is inside a for or while loop""" _node = node.parent while _node: - if isinstance(_node, (astng.For, astng.While)): + if isinstance(_node, (astroid.For, astroid.While)): break _node = _node.parent else: - self.add_message('E0103', node=node, args=node_name) + self.add_message('not-in-loop', node=node, args=node_name) def _check_redefinition(self, redeftype, node): """check for redefinition of a function / method / class name""" defined_self = node.parent.frame()[node.name] if defined_self is not node and not are_exclusive(node, defined_self): - self.add_message('E0102', node=node, + self.add_message('function-redefined', node=node, args=(redeftype, defined_self.fromlineno)) @@ -287,7 +330,6 @@ def _check_redefinition(self, redeftype, node): class BasicChecker(_BasicChecker): """checks for : * doc strings - * modules / classes / functions / methods / arguments / variables name * number of arguments, local variables, branches, returns and statements in functions, methods * required module attributes @@ -296,7 +338,7 @@ class BasicChecker(_BasicChecker): * uses of the global statement """ - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'basic' msgs = { @@ -332,11 +374,10 @@ class BasicChecker(_BasicChecker): 'duplicate-key', "Used when a dictionary expression binds the same key multiple \ times."), - 'W0122': ('Use of the exec statement', - 'exec-statement', - 'Used when you use the "exec" statement, to discourage its \ + 'W0122': ('Use of exec', + 'exec-used', + 'Used when you use the "exec" statement (function for Python 3), to discourage its \ usage. That doesn\'t mean you can not use it !'), - 'W0141': ('Used builtin function %r', 'bad-builtin', 'Used when a black listed builtin function is used (see the ' @@ -359,11 +400,15 @@ class BasicChecker(_BasicChecker): 'A call of assert on a tuple will always evaluate to true if ' 'the tuple is not empty, and will always evaluate to false if ' 'it is.'), + 'W0121': ('Use raise ErrorClass(args) instead of raise ErrorClass, args.', + 'old-raise-syntax', + "Used when the alternate raise syntax 'raise foo, bar' is used " + "instead of 'raise foo(bar)'.", + {'maxversion': (3, 0)}), 'C0121': ('Missing required attribute "%s"', # W0103 'missing-module-attribute', 'Used when an attribute required for modules is missing.'), - } options = (('required-attributes', @@ -392,14 +437,14 @@ def open(self): self._tryfinallys = [] self.stats = self.linter.add_stats(module=0, function=0, method=0, class_=0) - + @check_messages('missing-module-attribute') def visit_module(self, node): """check module name, docstring and required arguments """ self.stats['module'] += 1 for attr in self.config.required_attributes: if attr not in node: - self.add_message('C0121', node=node, args=attr) + self.add_message('missing-module-attribute', node=node, args=attr) def visit_class(self, node): """check module name, docstring and redefinition @@ -407,31 +452,32 @@ def visit_class(self, node): """ self.stats['class'] += 1 - @check_messages('W0104', 'W0105') + @check_messages('pointless-statement', 'pointless-string-statement', + 'expression-not-assigned') def visit_discard(self, node): """check for various kind of statements without effect""" expr = node.value - if isinstance(expr, astng.Const) and isinstance(expr.value, + if isinstance(expr, astroid.Const) and isinstance(expr.value, basestring): # treat string statement in a separated message - self.add_message('W0105', node=node) + self.add_message('pointless-string-statement', node=node) return # ignore if this is : # * a direct function call # * the unique child of a try/except body # * a yield (which are wrapped by a discard node in _ast XXX) # warn W0106 if we have any underlying function call (we can't predict - # side effects), else W0104 - if (isinstance(expr, (astng.Yield, astng.CallFunc)) or - (isinstance(node.parent, astng.TryExcept) and + # side effects), else pointless-statement + if (isinstance(expr, (astroid.Yield, astroid.CallFunc)) or + (isinstance(node.parent, astroid.TryExcept) and node.parent.body == [node])): return - if any(expr.nodes_of_class(astng.CallFunc)): - self.add_message('W0106', node=node, args=expr.as_string()) + if any(expr.nodes_of_class(astroid.CallFunc)): + self.add_message('expression-not-assigned', node=node, args=expr.as_string()) else: - self.add_message('W0104', node=node) + self.add_message('pointless-statement', node=node) - @check_messages('W0108') + @check_messages('unnecessary-lambda') def visit_lambda(self, node): """check whether or not the lambda is suspicious """ @@ -446,11 +492,11 @@ def visit_lambda(self, node): # of the lambda. return call = node.body - if not isinstance(call, astng.CallFunc): + if not isinstance(call, astroid.CallFunc): # The body of the lambda must be a function call expression # for the lambda to be unnecessary. return - # XXX are lambda still different with astng >= 0.18 ? + # XXX are lambda still different with astroid >= 0.18 ? # *args and **kwargs need to be treated specially, since they # are structured differently between the lambda and the function # call (in the lambda they appear in the args.args list and are @@ -460,14 +506,14 @@ def visit_lambda(self, node): ordinary_args = list(node.args.args) if node.args.kwarg: if (not call.kwargs - or not isinstance(call.kwargs, astng.Name) + or not isinstance(call.kwargs, astroid.Name) or node.args.kwarg != call.kwargs.name): return elif call.kwargs: return if node.args.vararg: if (not call.starargs - or not isinstance(call.starargs, astng.Name) + or not isinstance(call.starargs, astroid.Name) or node.args.vararg != call.starargs.name): return elif call.starargs: @@ -477,12 +523,13 @@ def visit_lambda(self, node): if len(ordinary_args) != len(call.args): return for i in xrange(len(ordinary_args)): - if not isinstance(call.args[i], astng.Name): + if not isinstance(call.args[i], astroid.Name): return if node.args.args[i].name != call.args[i].name: return - self.add_message('W0108', line=node.fromlineno, node=node) + self.add_message('unnecessary-lambda', line=node.fromlineno, node=node) + @check_messages('dangerous-default-value') def visit_function(self, node): """check function name, docstring, arguments, redefinition, variable names, max locals @@ -492,19 +539,20 @@ def visit_function(self, node): for default in node.args.defaults: try: value = default.infer().next() - except astng.InferenceError: + except astroid.InferenceError: continue - if (isinstance(value, astng.Instance) and - value.qname() in ('__builtin__.set', '__builtin__.dict', '__builtin__.list')): + builtins = bases.BUILTINS + if (isinstance(value, astroid.Instance) and + value.qname() in ['.'.join([builtins, x]) for x in ('set', 'dict', 'list')]): if value is default: msg = default.as_string() - elif type(value) is astng.Instance: + elif type(value) is astroid.Instance: msg = '%s (%s)' % (default.as_string(), value.qname()) else: msg = '%s (%s)' % (default.as_string(), value.as_string()) - self.add_message('W0102', node=node, args=(msg,)) + self.add_message('dangerous-default-value', node=node, args=(msg,)) - @check_messages('W0101', 'W0150') + @check_messages('unreachable', 'lost-exception') def visit_return(self, node): """1 - check is the node has a right sibling (if so, that's some unreachable code) @@ -513,16 +561,16 @@ def visit_return(self, node): """ self._check_unreachable(node) # Is it inside final body of a try...finally bloc ? - self._check_not_in_finally(node, 'return', (astng.Function,)) + self._check_not_in_finally(node, 'return', (astroid.Function,)) - @check_messages('W0101') + @check_messages('unreachable') def visit_continue(self, node): """check is the node has a right sibling (if so, that's some unreachable code) """ self._check_unreachable(node) - @check_messages('W0101', 'W0150') + @check_messages('unreachable', 'lost-exception') def visit_break(self, node): """1 - check is the node has a right sibling (if so, that's some unreachable code) @@ -532,36 +580,42 @@ def visit_break(self, node): # 1 - Is it right sibling ? self._check_unreachable(node) # 2 - Is it inside final body of a try...finally bloc ? - self._check_not_in_finally(node, 'break', (astng.For, astng.While,)) + self._check_not_in_finally(node, 'break', (astroid.For, astroid.While,)) - @check_messages('W0101') + @check_messages('unreachable', 'old-raise-syntax') def visit_raise(self, node): - """check is the node has a right sibling (if so, that's some unreachable + """check if the node has a right sibling (if so, that's some unreachable code) """ self._check_unreachable(node) + if sys.version_info >= (3, 0): + return + if node.exc is not None and node.inst is not None and node.tback is None: + self.add_message('old-raise-syntax', node=node) - @check_messages('W0122') + @check_messages('exec-used') def visit_exec(self, node): """just print a warning on exec statements""" - self.add_message('W0122', node=node) + self.add_message('exec-used', node=node) - @check_messages('W0141', 'W0142') + @check_messages('bad-builtin', 'star-args', 'exec-used') def visit_callfunc(self, node): """visit a CallFunc node -> check if this is not a blacklisted builtin call and check for * or ** use """ - if isinstance(node.func, astng.Name): + if isinstance(node.func, astroid.Name): name = node.func.name # ignore the name if it's not a builtin (i.e. not defined in the # locals nor globals scope) if not (name in node.frame() or name in node.root()): + if name == 'exec': + self.add_message('exec-used', node=node) if name in self.config.bad_functions: - self.add_message('W0141', node=node, args=name) + self.add_message('bad-builtin', node=node, args=name) if node.starargs or node.kwargs: scope = node.scope() - if isinstance(scope, astng.Function): + if isinstance(scope, astroid.Function): toprocess = [(n, vn) for (n, vn) in ((node.starargs, scope.args.vararg), (node.kwargs, scope.args.kwarg)) if n] if toprocess: @@ -569,25 +623,25 @@ def visit_callfunc(self, node): if getattr(cfnode, 'name', None) == fargname: toprocess.remove((cfnode, fargname)) if not toprocess: - return # W0142 can be skipped - self.add_message('W0142', node=node.func) + return # star-args can be skipped + self.add_message('star-args', node=node.func) - @check_messages('W0199') + @check_messages('assert-on-tuple') def visit_assert(self, node): """check the use of an assert statement on a tuple.""" - if node.fail is None and isinstance(node.test, astng.Tuple) and \ + if node.fail is None and isinstance(node.test, astroid.Tuple) and \ len(node.test.elts) == 2: - self.add_message('W0199', node=node) + self.add_message('assert-on-tuple', node=node) - @check_messages('W0109') + @check_messages('duplicate-key') def visit_dict(self, node): """check duplicate key in dictionary""" keys = set() for k, _ in node.items: - if isinstance(k, astng.Const): + if isinstance(k, astroid.Const): key = k.value if key in keys: - self.add_message('W0109', node=node, args=key) + self.add_message('duplicate-key', node=node, args=key) keys.add(key) def visit_tryfinally(self, node): @@ -602,7 +656,7 @@ def _check_unreachable(self, node): """check unreachable code""" unreach_stmt = node.next_sibling() if unreach_stmt is not None: - self.add_message('W0101', node=unreach_stmt) + self.add_message('unreachable', node=unreach_stmt) def _check_not_in_finally(self, node, node_name, breaker_classes=()): """check that a node is not inside a finally clause of a @@ -617,25 +671,24 @@ def _check_not_in_finally(self, node, node_name, breaker_classes=()): _node = node while _parent and not isinstance(_parent, breaker_classes): if hasattr(_parent, 'finalbody') and _node in _parent.finalbody: - self.add_message('W0150', node=node, args=node_name) + self.add_message('lost-exception', node=node, args=node_name) return _node = _parent _parent = _node.parent - class NameChecker(_BasicChecker): msgs = { 'C0102': ('Black listed name "%s"', 'blacklisted-name', 'Used when the name is listed in the black list (unauthorized \ names).'), - 'C0103': ('Invalid name "%s" for type %s (should match %s)', + 'C0103': ('Invalid %s name "%s"', 'invalid-name', 'Used when the name doesn\'t match the regular expression \ associated to its type (constant, variable, class...).'), - } + options = (('module-rgx', {'default' : MOD_NAME_RGX, 'type' :'regexp', 'metavar' : '', @@ -683,6 +736,12 @@ class NameChecker(_BasicChecker): 'help' : 'Regular expression which should only match correct ' 'variable names'} ), + ('class-attribute-rgx', + {'default' : CLASS_ATTRIBUTE_RGX, + 'type' :'regexp', 'metavar' : '', + 'help' : 'Regular expression which should only match correct ' + 'attribute names in class bodies'} + ), ('inlinevar-rgx', {'default' : COMP_VAR_RGX, 'type' :'regexp', 'metavar' : '', @@ -712,48 +771,65 @@ def open(self): badname_const=0, badname_variable=0, badname_inlinevar=0, - badname_argument=0) + badname_argument=0, + badname_class_attribute=0) - @check_messages('C0102', 'C0103') + @check_messages('blacklisted-name', 'invalid-name') def visit_module(self, node): self._check_name('module', node.name.split('.')[-1], node) - @check_messages('C0102', 'C0103') + @check_messages('blacklisted-name', 'invalid-name') def visit_class(self, node): self._check_name('class', node.name, node) for attr, anodes in node.instance_attrs.iteritems(): - self._check_name('attr', attr, anodes[0]) + if not list(node.instance_attr_ancestors(attr)): + self._check_name('attr', attr, anodes[0]) - @check_messages('C0102', 'C0103') + @check_messages('blacklisted-name', 'invalid-name') def visit_function(self, node): - self._check_name(node.is_method() and 'method' or 'function', + # Do not emit any warnings if the method is just an implementation + # of a base class method. + if node.is_method() and overrides_a_method(node.parent.frame(), node.name): + return + self._check_name(_determine_function_name_type(node), node.name, node) - # check arguments name + # Check argument names args = node.args.args if args is not None: self._recursive_check_names(args, node) - @check_messages('C0102', 'C0103') + @check_messages('blacklisted-name', 'invalid-name') + def visit_global(self, node): + for name in node.names: + self._check_name('const', name, node) + + @check_messages('blacklisted-name', 'invalid-name') def visit_assname(self, node): """check module level assigned names""" frame = node.frame() ass_type = node.ass_type() - if isinstance(ass_type, (astng.Comprehension, astng.Comprehension)): + if isinstance(ass_type, astroid.Comprehension): self._check_name('inlinevar', node.name, node) - elif isinstance(frame, astng.Module): - if isinstance(ass_type, astng.Assign) and not in_loop(ass_type): - self._check_name('const', node.name, node) - elif isinstance(ass_type, astng.ExceptHandler): + elif isinstance(frame, astroid.Module): + if isinstance(ass_type, astroid.Assign) and not in_loop(ass_type): + if isinstance(safe_infer(ass_type.value), astroid.Class): + self._check_name('class', node.name, node) + else: + self._check_name('const', node.name, node) + elif isinstance(ass_type, astroid.ExceptHandler): self._check_name('variable', node.name, node) - elif isinstance(frame, astng.Function): + elif isinstance(frame, astroid.Function): # global introduced variable aren't in the function locals - if node.name in frame: + if node.name in frame and node.name not in frame.argnames(): self._check_name('variable', node.name, node) + elif isinstance(frame, astroid.Class): + if not list(frame.local_attr_ancestors(node.name)): + self._check_name('class_attribute', node.name, node) def _recursive_check_names(self, args, node): """check names in a possibly recursive list """ for arg in args: - if isinstance(arg, astng.AssName): + if isinstance(arg, astroid.AssName): self._check_name('argument', arg.name, node) else: self._recursive_check_names(arg.elts, node) @@ -768,26 +844,27 @@ def _check_name(self, node_type, name, node): return if name in self.config.bad_names: self.stats['badname_' + node_type] += 1 - self.add_message('C0102', node=node, args=name) + self.add_message('blacklisted-name', node=node, args=name) return regexp = getattr(self.config, node_type + '_rgx') if regexp.match(name) is None: type_label = {'inlinedvar': 'inlined variable', 'const': 'constant', 'attr': 'attribute', + 'class_attribute': 'class attribute' }.get(node_type, node_type) - self.add_message('C0103', node=node, args=(name, type_label, regexp.pattern)) + self.add_message('invalid-name', node=node, args=(type_label, name)) self.stats['badname_' + node_type] += 1 class DocStringChecker(_BasicChecker): msgs = { - 'C0111': ('Missing docstring', # W0131 + 'C0111': ('Missing %s docstring', # W0131 'missing-docstring', 'Used when a module, function, class or method has no docstring.\ Some special methods like __init__ doesn\'t necessary require a \ docstring.'), - 'C0112': ('Empty docstring', # W0132 + 'C0112': ('Empty %s docstring', # W0132 'empty-docstring', 'Used when a module, function, class or method has an empty \ docstring (it would be too easy ;).'), @@ -796,33 +873,41 @@ class DocStringChecker(_BasicChecker): {'default' : NO_REQUIRED_DOC_RGX, 'type' : 'regexp', 'metavar' : '', 'help' : 'Regular expression which should only match ' - 'functions or classes name which do not require a ' - 'docstring'} + 'function or class names that do not require a ' + 'docstring.'} + ), + ('docstring-min-length', + {'default' : -1, + 'type' : 'int', 'metavar' : '', + 'help': ('Minimum line length for functions/classes that' + ' require docstrings, shorter ones are exempt.')} ), ) + def open(self): self.stats = self.linter.add_stats(undocumented_module=0, undocumented_function=0, undocumented_method=0, undocumented_class=0) - + @check_messages('missing-docstring', 'empty-docstring') def visit_module(self, node): self._check_docstring('module', node) + @check_messages('missing-docstring', 'empty-docstring') def visit_class(self, node): if self.config.no_docstring_rgx.match(node.name) is None: self._check_docstring('class', node) - + @check_messages('missing-docstring', 'empty-docstring') def visit_function(self, node): if self.config.no_docstring_rgx.match(node.name) is None: ftype = node.is_method() and 'method' or 'function' - if isinstance(node.parent.frame(), astng.Class): + if isinstance(node.parent.frame(), astroid.Class): overridden = False # check if node is from a method overridden by its ancestor for ancestor in node.parent.frame().ancestors(): if node.name in ancestor and \ - isinstance(ancestor[node.name], astng.Function): + isinstance(ancestor[node.name], astroid.Function): overridden = True break if not overridden: @@ -834,24 +919,32 @@ def _check_docstring(self, node_type, node): """check the node has a non empty docstring""" docstring = node.doc if docstring is None: + if node.body: + lines = node.body[-1].lineno - node.body[0].lineno + 1 + else: + lines = 0 + max_lines = self.config.docstring_min_length + + if node_type != 'module' and max_lines > -1 and lines < max_lines: + return self.stats['undocumented_'+node_type] += 1 - self.add_message('C0111', node=node) + self.add_message('missing-docstring', node=node, args=(node_type,)) elif not docstring.strip(): self.stats['undocumented_'+node_type] += 1 - self.add_message('C0112', node=node) + self.add_message('empty-docstring', node=node, args=(node_type,)) class PassChecker(_BasicChecker): - """check is the pass statement is really necessary""" + """check if the pass statement is really necessary""" msgs = {'W0107': ('Unnecessary pass statement', 'unnecessary-pass', 'Used when a "pass" statement that can be avoided is ' 'encountered.'), } - + @check_messages('unnecessary-pass') def visit_pass(self, node): if len(node.parent.child_sequence(node)) > 1: - self.add_message('W0107', node=node) + self.add_message('unnecessary-pass', node=node) class LambdaForComprehensionChecker(_BasicChecker): @@ -865,23 +958,23 @@ class LambdaForComprehensionChecker(_BasicChecker): 'deprecated-lambda', 'Used when a lambda is the first argument to "map" or ' '"filter". It could be clearer as a list ' - 'comprehension or generator expression.'), + 'comprehension or generator expression.', + {'maxversion': (3, 0)}), } - @check_messages('W0110') + @check_messages('deprecated-lambda') def visit_callfunc(self, node): """visit a CallFunc node, check if map or filter are called with a lambda """ if not node.args: return - if not isinstance(node.args[0], astng.Lambda): + if not isinstance(node.args[0], astroid.Lambda): return infered = safe_infer(node.func) - if (infered - and infered.parent.name == '__builtin__' + if (is_builtin_object(infered) and infered.name in ['map', 'filter']): - self.add_message('W0110', node=node) + self.add_message('deprecated-lambda', node=node) def register(linter): diff --git a/pylibs/pylama/checkers/pylint/checkers/classes.py b/pylibs/pylama/checkers/pylint/checkers/classes.py index 8e3b836d..ecacecea 100644 --- a/pylibs/pylama/checkers/pylint/checkers/classes.py +++ b/pylibs/pylama/checkers/pylint/checkers/classes.py @@ -17,12 +17,12 @@ """ from __future__ import generators -from ..logilab import astng -from ..logilab.astng import YES, Instance, are_exclusive, AssAttr +from .. import astroid +from ..astroid import YES, Instance, are_exclusive, AssAttr -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker -from ..checkers.utils import (PYMETHODS, overrides_a_method, +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import (PYMETHODS, overrides_a_method, check_messages, is_attr_private, is_attr_protected, node_frame_class) def class_is_abstract(node): @@ -156,7 +156,7 @@ class ClassChecker(BaseChecker): * unreachable code """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) # configuration section name name = 'classes' @@ -222,7 +222,7 @@ def visit_class(self, node): if node.type == 'class': try: node.local_attr('__init__') - except astng.NotFoundError: + except astroid.NotFoundError: self.add_message('W0232', args=node, node=node) @check_messages('E0203', 'W0201') @@ -241,7 +241,7 @@ def leave_class(self, cnode): defining_methods = self.config.defining_attr_methods for attr, nodes in cnode.instance_attrs.iteritems(): nodes = [n for n in nodes if not - isinstance(n.statement(), (astng.Delete, astng.AugAssign))] + isinstance(n.statement(), (astroid.Delete, astroid.AugAssign))] if not nodes: continue # error detected by typechecking attr_defined = False @@ -264,7 +264,7 @@ def leave_class(self, cnode): # check attribute is defined as a class attribute try: cnode.local_attr(attr) - except astng.NotFoundError: + except astroid.NotFoundError: self.add_message('W0201', args=attr, node=node) def visit_function(self, node): @@ -281,25 +281,25 @@ def visit_function(self, node): return # check signature if the method overloads inherited method for overridden in klass.local_attr_ancestors(node.name): - # get astng for the searched method + # get astroid for the searched method try: meth_node = overridden[node.name] except KeyError: # we have found the method but it's not in the local # dictionary. - # This may happen with astng build from living objects + # This may happen with astroid build from living objects continue - if not isinstance(meth_node, astng.Function): + if not isinstance(meth_node, astroid.Function): continue self._check_signature(node, meth_node, 'overridden') break if node.decorators: for decorator in node.decorators.nodes: - if isinstance(decorator, astng.Getattr) and \ + if isinstance(decorator, astroid.Getattr) and \ decorator.attrname in ('getter', 'setter', 'deleter'): # attribute affectation will call this method, not hiding it return - if isinstance(decorator, astng.Name) and decorator.name == 'property': + if isinstance(decorator, astroid.Name) and decorator.name == 'property': # attribute affectation will either call a setter or raise # an attribute error, anyway not hiding the function return @@ -308,7 +308,7 @@ def visit_function(self, node): overridden = klass.instance_attr(node.name)[0] # XXX args = (overridden.root().name, overridden.fromlineno) self.add_message('E0202', args=args, node=node) - except astng.NotFoundError: + except astroid.NotFoundError: pass def leave_function(self, node): @@ -387,8 +387,8 @@ def _check_protected_attribute_access(self, node): return # If the expression begins with a call to super, that's ok. - if isinstance(node.expr, astng.CallFunc) and \ - isinstance(node.expr.func, astng.Name) and \ + if isinstance(node.expr, astroid.CallFunc) and \ + isinstance(node.expr.func, astroid.Name) and \ node.expr.func.name == 'super': return @@ -416,7 +416,7 @@ def _check_accessed_members(self, node, accessed): node.local_attr(attr) # yes, stop here continue - except astng.NotFoundError: + except astroid.NotFoundError: pass # is it an instance attribute of a parent class ? try: @@ -428,7 +428,7 @@ def _check_accessed_members(self, node, accessed): # is it an instance attribute ? try: defstmts = node.instance_attr(attr) - except astng.NotFoundError: + except astroid.NotFoundError: pass else: if len(defstmts) == 1: @@ -530,7 +530,7 @@ def _check_interfaces(self, node): e0221_hack = [False] def iface_handler(obj): """filter interface objects, it should be classes""" - if not isinstance(obj, astng.Class): + if not isinstance(obj, astroid.Class): e0221_hack[0] = True self.add_message('E0221', node=node, args=(obj.as_string(),)) @@ -545,10 +545,10 @@ def iface_handler(obj): # don't check method beginning with an underscore, # usually belonging to the interface implementation continue - # get class method astng + # get class method astroid try: method = node_method(node, name) - except astng.NotFoundError: + except astroid.NotFoundError: self.add_message('E0222', args=(name, iface.name), node=node) continue @@ -558,12 +558,12 @@ def iface_handler(obj): # check signature self._check_signature(method, imethod, '%s interface' % iface.name) - except astng.InferenceError: + except astroid.InferenceError: if e0221_hack[0]: return implements = Instance(node).getattr('__implements__')[0] assignment = implements.parent - assert isinstance(assignment, astng.Assign) + assert isinstance(assignment, astroid.Assign) # assignment.expr can be a Name or a Tuple or whatever. # Use as_string() for the message # FIXME: in case of multiple interfaces, find which one could not @@ -580,14 +580,14 @@ def _check_init(self, node): klass_node = node.parent.frame() to_call = _ancestors_to_call(klass_node) not_called_yet = dict(to_call) - for stmt in node.nodes_of_class(astng.CallFunc): + for stmt in node.nodes_of_class(astroid.CallFunc): expr = stmt.func - if not isinstance(expr, astng.Getattr) \ + if not isinstance(expr, astroid.Getattr) \ or expr.attrname != '__init__': continue # skip the test if using super - if isinstance(expr.expr, astng.CallFunc) and \ - isinstance(expr.expr.func, astng.Name) and \ + if isinstance(expr.expr, astroid.CallFunc) and \ + isinstance(expr.expr.func, astroid.Name) and \ expr.expr.func.name == 'super': return try: @@ -599,7 +599,7 @@ def _check_init(self, node): except KeyError: if klass not in to_call: self.add_message('W0233', node=expr, args=klass.name) - except astng.InferenceError: + except astroid.InferenceError: continue for klass, method in not_called_yet.iteritems(): if klass.name == 'object' or method.parent.name == 'object': @@ -611,8 +611,8 @@ def _check_signature(self, method1, refmethod, class_type): class_type is in 'class', 'interface' """ - if not (isinstance(method1, astng.Function) - and isinstance(refmethod, astng.Function)): + if not (isinstance(method1, astroid.Function) + and isinstance(refmethod, astroid.Function)): self.add_message('F0202', args=(method1, refmethod), node=method1) return # don't care about functions with unknown argument (builtins) @@ -632,7 +632,7 @@ def is_first_attr(self, node): """Check that attribute lookup name use first attribute variable name (self for method, cls for classmethod and mcs for metaclass). """ - return self._first_attrs and isinstance(node.expr, astng.Name) and \ + return self._first_attrs and isinstance(node.expr, astroid.Name) and \ node.expr.name == self._first_attrs[-1] def _ancestors_to_call(klass_node, method='__init__'): @@ -643,19 +643,19 @@ def _ancestors_to_call(klass_node, method='__init__'): for base_node in klass_node.ancestors(recurs=False): try: to_call[base_node] = base_node.igetattr(method).next() - except astng.InferenceError: + except astroid.InferenceError: continue return to_call def node_method(node, method_name): - """get astng for on the given class node, ensuring it + """get astroid for on the given class node, ensuring it is a Function node """ for n in node.local_attr(method_name): - if isinstance(n, astng.Function): + if isinstance(n, astroid.Function): return n - raise astng.NotFoundError(method_name) + raise astroid.NotFoundError(method_name) def register(linter): """required method to auto register this checker """ diff --git a/pylibs/pylama/checkers/pylint/checkers/design_analysis.py b/pylibs/pylama/checkers/pylint/checkers/design_analysis.py index 789cf88f..f81efc37 100644 --- a/pylibs/pylama/checkers/pylint/checkers/design_analysis.py +++ b/pylibs/pylama/checkers/pylint/checkers/design_analysis.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -13,17 +13,13 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -"""check for signs of poor design +"""check for signs of poor design""" +from ..astroid import Function, If, InferenceError - see http://intranet.logilab.fr/jpl/view?rql=Any%20X%20where%20X%20eid%201243 - FIXME: missing 13, 15, 16 -""" - -from ..logilab.astng import Function, If, InferenceError - -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import check_messages import re @@ -33,10 +29,9 @@ SPECIAL_METHODS = [('Context manager', set(('__enter__', '__exit__',))), ('Container', set(('__len__', - '__getitem__', - '__setitem__', - '__delitem__',))), - ('Callable', set(('__call__',))), + '__getitem__',))), + ('Mutable container', set(('__setitem__', + '__delitem__',))), ] class SpecialMethodChecker(object): @@ -60,8 +55,8 @@ def __call__(self, methods_required, protocol): required_methods_found = methods_required & self.methods_found if required_methods_found == methods_required: return True - if required_methods_found != set(): - required_methods_missing = methods_required - self.methods_found + if required_methods_found: + required_methods_missing = methods_required - self.methods_found self.on_error((protocol, ', '.join(sorted(required_methods_found)), ', '.join(sorted(required_methods_missing)))) @@ -83,11 +78,11 @@ def class_is_abstract(klass): 'R0901': ('Too many ancestors (%s/%s)', 'too-many-ancestors', 'Used when class has too many parent classes, try to reduce \ - this to get a more simple (and so easier to use) class.'), + this to get a simpler (and so easier to use) class.'), 'R0902': ('Too many instance attributes (%s/%s)', 'too-many-instance-attributes', 'Used when class has too many instance attributes, try to reduce \ - this to get a more simple (and so easier to use) class.'), + this to get a simpler (and so easier to use) class.'), 'R0903': ('Too few public methods (%s/%s)', 'too-few-public-methods', 'Used when class has too few public methods, so be sure it\'s \ @@ -95,7 +90,7 @@ def class_is_abstract(klass): 'R0904': ('Too many public methods (%s/%s)', 'too-many-public-methods', 'Used when class has too many public methods, try to reduce \ - this to get a more simple (and so easier to use) class.'), + this to get a simpler (and so easier to use) class.'), 'R0911': ('Too many return statements (%s/%s)', 'too-many-return-statements', @@ -139,7 +134,7 @@ class MisdesignChecker(BaseChecker): * size, complexity of functions, methods """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) # configuration section name name = 'design' @@ -166,7 +161,7 @@ class MisdesignChecker(BaseChecker): 'help': 'Maximum number of return / yield for function / ' 'method body'} ), - ('max-branchs', + ('max-branches', {'default' : 12, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of branch for function / method body'} ), @@ -208,7 +203,7 @@ def __init__(self, linter=None): BaseChecker.__init__(self, linter) self.stats = None self._returns = None - self._branchs = None + self._branches = None self._used_abstracts = None self._used_ifaces = None self._abstracts = None @@ -219,12 +214,13 @@ def open(self): """initialize visit variables""" self.stats = self.linter.add_stats() self._returns = [] - self._branchs = [] + self._branches = [] self._used_abstracts = {} self._used_ifaces = {} self._abstracts = [] self._ifaces = [] + # Check 'R0921', 'R0922', 'R0923' def close(self): """check that abstract/interface classes are used""" for abstract in self._abstracts: @@ -237,6 +233,7 @@ def close(self): if not iface in self._used_ifaces: self.add_message('R0923', node=iface) + @check_messages('R0901', 'R0902', 'R0903', 'R0904', 'R0921', 'R0922', 'R0923') def visit_class(self, node): """check size of inheritance hierarchy and number of instance attributes """ @@ -274,6 +271,7 @@ def visit_class(self, node): except KeyError: self._used_abstracts[parent] = 1 + @check_messages('R0901', 'R0902', 'R0903', 'R0904', 'R0921', 'R0922', 'R0923') def leave_class(self, node): """check number of public methods""" nb_public_methods = 0 @@ -304,6 +302,7 @@ def leave_class(self, node): args=(nb_public_methods, self.config.min_public_methods)) + @check_messages('R0911', 'R0912', 'R0913', 'R0914', 'R0915') def visit_function(self, node): """check function name, docstring, arguments, redefinition, variable names, max locals @@ -311,7 +310,7 @@ def visit_function(self, node): self._inc_branch() # init branch and returns counters self._returns.append(0) - self._branchs.append(0) + self._branches.append(0) # check number of arguments args = node.args.args if args is not None: @@ -332,6 +331,7 @@ def visit_function(self, node): # init statements counter self._stmts = 1 + @check_messages('R0911', 'R0912', 'R0913', 'R0914', 'R0915') def leave_function(self, node): """most of the work is done here on close: checks for max returns, branch, return in __init__ @@ -340,10 +340,10 @@ def leave_function(self, node): if returns > self.config.max_returns: self.add_message('R0911', node=node, args=(returns, self.config.max_returns)) - branchs = self._branchs.pop() - if branchs > self.config.max_branchs: + branches = self._branches.pop() + if branches > self.config.max_branches: self.add_message('R0912', node=node, - args=(branchs, self.config.max_branchs)) + args=(branches, self.config.max_branches)) # check number of statements if self._stmts > self.config.max_statements: self.add_message('R0915', node=node, @@ -363,42 +363,42 @@ def visit_default(self, node): self._stmts += 1 def visit_tryexcept(self, node): - """increments the branchs counter""" - branchs = len(node.handlers) + """increments the branches counter""" + branches = len(node.handlers) if node.orelse: - branchs += 1 - self._inc_branch(branchs) - self._stmts += branchs + branches += 1 + self._inc_branch(branches) + self._stmts += branches def visit_tryfinally(self, _): - """increments the branchs counter""" + """increments the branches counter""" self._inc_branch(2) self._stmts += 2 def visit_if(self, node): - """increments the branchs counter""" - branchs = 1 + """increments the branches counter""" + branches = 1 # don't double count If nodes coming from some 'elif' if node.orelse and (len(node.orelse)>1 or not isinstance(node.orelse[0], If)): - branchs += 1 - self._inc_branch(branchs) - self._stmts += branchs + branches += 1 + self._inc_branch(branches) + self._stmts += branches def visit_while(self, node): - """increments the branchs counter""" - branchs = 1 + """increments the branches counter""" + branches = 1 if node.orelse: - branchs += 1 - self._inc_branch(branchs) + branches += 1 + self._inc_branch(branches) visit_for = visit_while - def _inc_branch(self, branchsnum=1): - """increments the branchs counter""" - branchs = self._branchs - for i in xrange(len(branchs)): - branchs[i] += branchsnum + def _inc_branch(self, branchesnum=1): + """increments the branches counter""" + branches = self._branches + for i in xrange(len(branches)): + branches[i] += branchesnum # FIXME: make a nice report... diff --git a/pylibs/pylama/checkers/pylint/checkers/exceptions.py b/pylibs/pylama/checkers/pylint/checkers/exceptions.py index b2006cef..3031075f 100644 --- a/pylibs/pylama/checkers/pylint/checkers/exceptions.py +++ b/pylibs/pylama/checkers/pylint/checkers/exceptions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2007 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software @@ -18,12 +18,12 @@ from ..logilab.common.compat import builtins BUILTINS_NAME = builtins.__name__ -from ..logilab import astng -from ..logilab.astng import YES, Instance, unpack_infer +from .. import astroid +from ..astroid import YES, Instance, unpack_infer -from ..checkers import BaseChecker -from ..checkers.utils import is_empty, is_raising -from ..interfaces import IASTNGChecker +from . import BaseChecker +from .utils import is_empty, is_raising, check_messages +from ..interfaces import IAstroidChecker OVERGENERAL_EXCEPTIONS = ('Exception',) @@ -71,6 +71,12 @@ 'Used when the exception to catch is of the form \ "except A or B:". If intending to catch multiple, \ rewrite as "except (A, B):"'), + 'W0712': ('Implicit unpacking of exceptions is not supported in Python 3', + 'unpacking-in-except', + 'Python3 will not allow implicit unpacking of exceptions in except ' + 'clauses. ' + 'See http://www.python.org/dev/peps/pep-3110/', + {'maxversion': (3, 0)}), } @@ -85,7 +91,7 @@ class ExceptionsChecker(BaseChecker): * type of raise argument : string, Exceptions, other values """ - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'exceptions' msgs = MSGS @@ -99,6 +105,7 @@ class ExceptionsChecker(BaseChecker): ), ) + @check_messages('W0701', 'W0710', 'E0702', 'E0710', 'E0711') def visit_raise(self, node): """visit raise possibly inferring value""" # ignore empty raise @@ -110,7 +117,7 @@ def visit_raise(self, node): else: try: value = unpack_infer(expr).next() - except astng.InferenceError: + except astroid.InferenceError: return self._check_raise_value(node, value) @@ -118,29 +125,29 @@ def _check_raise_value(self, node, expr): """check for bad values, string exception and class inheritance """ value_found = True - if isinstance(expr, astng.Const): + if isinstance(expr, astroid.Const): value = expr.value if isinstance(value, str): self.add_message('W0701', node=node) else: self.add_message('E0702', node=node, args=value.__class__.__name__) - elif (isinstance(expr, astng.Name) and \ + elif (isinstance(expr, astroid.Name) and \ expr.name in ('None', 'True', 'False')) or \ - isinstance(expr, (astng.List, astng.Dict, astng.Tuple, - astng.Module, astng.Function)): + isinstance(expr, (astroid.List, astroid.Dict, astroid.Tuple, + astroid.Module, astroid.Function)): self.add_message('E0702', node=node, args=expr.name) - elif ( (isinstance(expr, astng.Name) and expr.name == 'NotImplemented') - or (isinstance(expr, astng.CallFunc) and - isinstance(expr.func, astng.Name) and + elif ( (isinstance(expr, astroid.Name) and expr.name == 'NotImplemented') + or (isinstance(expr, astroid.CallFunc) and + isinstance(expr.func, astroid.Name) and expr.func.name == 'NotImplemented') ): self.add_message('E0711', node=node) - elif isinstance(expr, astng.BinOp) and expr.op == '%': + elif isinstance(expr, astroid.BinOp) and expr.op == '%': self.add_message('W0701', node=node) - elif isinstance(expr, (Instance, astng.Class)): + elif isinstance(expr, (Instance, astroid.Class)): if isinstance(expr, Instance): expr = expr._proxied - if (isinstance(expr, astng.Class) and + if (isinstance(expr, astroid.Class) and not inherit_from_std_ex(expr) and expr.root().name != BUILTINS_NAME): if expr.newstyle: @@ -154,6 +161,12 @@ def _check_raise_value(self, node, expr): return value_found + @check_messages('W0712') + def visit_excepthandler(self, node): + """Visit an except handler block and check for exception unpacking.""" + if isinstance(node.name, (astroid.Tuple, astroid.List)): + self.add_message('W0712', node=node) + @check_messages('W0702', 'W0703', 'W0704', 'W0711', 'E0701') def visit_tryexcept(self, node): """check for empty except""" exceptions_classes = [] @@ -171,19 +184,19 @@ def visit_tryexcept(self, node): msg = 'empty except clause should always appear last' self.add_message('E0701', node=node, args=msg) - elif isinstance(handler.type, astng.BoolOp): + elif isinstance(handler.type, astroid.BoolOp): self.add_message('W0711', node=handler, args=handler.type.op) else: try: excs = list(unpack_infer(handler.type)) - except astng.InferenceError: + except astroid.InferenceError: continue for exc in excs: # XXX skip other non class nodes - if exc is YES or not isinstance(exc, astng.Class): + if exc is YES or not isinstance(exc, astroid.Class): continue exc_ancestors = [anc for anc in exc.ancestors() - if isinstance(anc, astng.Class)] + if isinstance(anc, astroid.Class)] for previous_exc in exceptions_classes: if previous_exc in exc_ancestors: msg = '%s is an ancestor class of %s' % ( diff --git a/pylibs/pylama/checkers/pylint/checkers/format.py b/pylibs/pylama/checkers/pylint/checkers/format.py index 83da2613..bf53234a 100644 --- a/pylibs/pylama/checkers/pylint/checkers/format.py +++ b/pylibs/pylama/checkers/pylint/checkers/format.py @@ -27,12 +27,12 @@ raise ValueError("tokenize.NL doesn't exist -- tokenize module too old") from ..logilab.common.textutils import pretty_match -from ..logilab.astng import nodes +from ..astroid import nodes -from ..interfaces import IRawChecker, IASTNGChecker -from ..checkers import BaseRawChecker -from ..checkers.utils import check_messages -from ..utils import WarningScope +from ..interfaces import ITokenChecker, IAstroidChecker +from . import BaseTokenChecker +from .utils import check_messages +from ..utils import WarningScope, OPTION_RGX MSGS = { 'C0301': ('Line too long (%s/%s)', @@ -42,7 +42,13 @@ 'too-many-lines', 'Used when a module has too much lines, reducing its readability.' ), - + 'C0303': ('Trailing whitespace', + 'trailing-whitespace', + 'Used when there is whitespace between the end of a line and the ' + 'newline.'), + 'C0304': ('Final newline missing', + 'missing-final-newline', + 'Used when the last line in a file is missing a newline.'), 'W0311': ('Bad indentation. Found %s %s, expected %s', 'bad-indentation', 'Used when an unexpected number of indentation\'s tabulations or ' @@ -163,7 +169,7 @@ def check_line(line): return msg_id, pretty_match(match, line.rstrip()) -class FormatChecker(BaseRawChecker): +class FormatChecker(BaseTokenChecker): """checks for : * unauthorized constructions * strict indentation @@ -171,7 +177,7 @@ class FormatChecker(BaseRawChecker): * use of <> instead of != """ - __implements__ = (IRawChecker, IASTNGChecker) + __implements__ = (ITokenChecker, IAstroidChecker) # configuration section name name = 'format' @@ -182,6 +188,11 @@ class FormatChecker(BaseRawChecker): options = (('max-line-length', {'default' : 80, 'type' : "int", 'metavar' : '', 'help' : 'Maximum number of characters on a single line.'}), + ('ignore-long-lines', + {'type': 'regexp', 'metavar': '', + 'default': r'^\s*(# )??$', + 'help': ('Regexp for a line that is allowed to be longer than ' + 'the limit.')}), ('max-module-lines', {'default' : 1000, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of lines in a module'} @@ -192,22 +203,10 @@ class FormatChecker(BaseRawChecker): " " (4 spaces) or "\\t" (1 tab).'}), ) def __init__(self, linter=None): - BaseRawChecker.__init__(self, linter) + BaseTokenChecker.__init__(self, linter) self._lines = None self._visited_lines = None - def process_module(self, node): - """extracts encoding from the stream and decodes each line, so that - international text's length is properly calculated. - """ - stream = node.file_stream - stream.seek(0) # XXX may be removed with astng > 0.23 - readline = stream.readline - if sys.version_info < (3, 0): - if node.file_encoding is not None: - readline = lambda: stream.readline().decode(node.file_encoding, 'replace') - self.process_tokens(tokenize.generate_tokens(readline)) - def new_line(self, tok_type, line, line_num, junk): """a new line has been encountered, process it if necessary""" if not tok_type in junk: @@ -233,13 +232,24 @@ def process_tokens(self, tokens): previous = None self._lines = {} self._visited_lines = {} + new_line_delay = False for (tok_type, token, start, _, line) in tokens: + if new_line_delay: + new_line_delay = False + self.new_line(tok_type, line, line_num, junk) if start[0] != line_num: if previous is not None and previous[0] == tokenize.OP and previous[1] == ';': self.add_message('W0301', line=previous[2]) previous = None line_num = start[0] - self.new_line(tok_type, line, line_num, junk) + # A tokenizer oddity: if an indented line contains a multi-line + # docstring, the line member of the INDENT token does not contain + # the full line; therefore we delay checking the new line until + # the next token. + if tok_type == tokenize.INDENT: + new_line_delay = True + else: + self.new_line(tok_type, line, line_num, junk) if tok_type not in (indent, dedent, newline) + junk: previous = tok_type, token, start[0] @@ -338,8 +348,22 @@ def check_lines(self, lines, i): """check lines have less than a maximum number of characters """ max_chars = self.config.max_line_length - for line in lines.splitlines(): - if len(line) > max_chars: + ignore_long_line = self.config.ignore_long_lines + + for line in lines.splitlines(True): + if not line.endswith('\n'): + self.add_message('C0304', line=i) + else: + stripped_line = line.rstrip() + if line != stripped_line + '\n': + self.add_message('C0303', line=i) + # Don't count excess whitespace in the line length. + line = stripped_line + mobj = OPTION_RGX.search(line) + if mobj and mobj.group(1).split('=', 1)[0].strip() == 'disable': + line = line.split('#')[0].rstrip() + + if len(line) > max_chars and not ignore_long_line.search(line): self.add_message('C0301', line=i, args=(len(line), max_chars)) i += 1 diff --git a/pylibs/pylama/checkers/pylint/checkers/imports.py b/pylibs/pylama/checkers/pylint/checkers/imports.py index e243ff6b..3c321bbb 100644 --- a/pylibs/pylama/checkers/pylint/checkers/imports.py +++ b/pylibs/pylama/checkers/pylint/checkers/imports.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -19,11 +19,13 @@ from ..logilab.common.modutils import is_standard_module from ..logilab.common.ureports import VerbatimText, Paragraph -from ..logilab import astng -from ..logilab.astng import are_exclusive +from .. import astroid +from ..astroid import are_exclusive -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker, EmptyReport +from ..interfaces import IAstroidChecker +from ..utils import EmptyReport +from . import BaseChecker +from .utils import check_messages def get_first_import(node, context, name, base, level): @@ -38,11 +40,11 @@ def get_first_import(node, context, name, base, level): continue if first.scope() is node.scope() and first.fromlineno > node.fromlineno: continue - if isinstance(first, astng.Import): + if isinstance(first, astroid.Import): if any(fullname == iname[0] for iname in first.names): found = True break - elif isinstance(first, astng.From): + elif isinstance(first, astroid.From): if level == first.level and any( fullname == '%s.%s' % (first.modname, iname[0]) for iname in first.names): found = True @@ -157,15 +159,14 @@ class ImportsChecker(BaseChecker): * uses of deprecated modules """ - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'imports' msgs = MSGS priority = -2 options = (('deprecated-modules', - {'default' : ('regsub', 'string', 'TERMIOS', - 'Bastion', 'rexec'), + {'default' : ('regsub', 'TERMIOS', 'Bastion', 'rexec'), 'type' : 'csv', 'metavar' : '', 'help' : 'Deprecated modules which should not be used, \ @@ -232,7 +233,9 @@ def visit_import(self, node): self._check_deprecated_module(node, name) self._check_reimport(node, name) - + # TODO This appears to be the list of all messages of the checker... + # @check_messages('W0410', 'W0401', 'W0403', 'W0402', 'W0404', 'W0406', 'F0401') + @check_messages(*(MSGS.keys())) def visit_from(self, node): """triggered when a from statement is seen""" basename = node.modname @@ -241,7 +244,7 @@ def visit_from(self, node): prev = node.previous_sibling() if prev: # consecutive future statements are possible - if not (isinstance(prev, astng.From) + if not (isinstance(prev, astroid.From) and prev.modname == '__future__'): self.add_message('W0410', node=node) return @@ -261,7 +264,7 @@ def visit_from(self, node): def get_imported_module(self, modnode, importnode, modname): try: return importnode.do_import_module(modname) - except astng.InferenceError, ex: + except astroid.InferenceError, ex: if str(ex) != modname: args = '%r (%s)' % (modname, ex) else: diff --git a/pylibs/pylama/checkers/pylint/checkers/logging.py b/pylibs/pylama/checkers/pylint/checkers/logging.py index bb95a539..5f2381ca 100644 --- a/pylibs/pylama/checkers/pylint/checkers/logging.py +++ b/pylibs/pylama/checkers/pylint/checkers/logging.py @@ -14,11 +14,9 @@ """checker for use of Python logging """ -from ..logilab import astng -from .. import checkers +from .. import astroid +from . import BaseChecker, utils from .. import interfaces -from ..checkers import utils - MSGS = { 'W1201': ('Specify string format arguments as logging function parameters', @@ -54,10 +52,10 @@ 'warning']) -class LoggingChecker(checkers.BaseChecker): +class LoggingChecker(BaseChecker): """Checks use of the logging module.""" - __implements__ = interfaces.IASTNGChecker + __implements__ = interfaces.IAstroidChecker name = 'logging' msgs = MSGS @@ -77,18 +75,19 @@ def visit_import(self, node): else: self._logging_name = 'logging' + @utils.check_messages(*(MSGS.keys())) def visit_callfunc(self, node): """Checks calls to (simple forms of) logging methods.""" - if (not isinstance(node.func, astng.Getattr) - or not isinstance(node.func.expr, astng.Name)): + if (not isinstance(node.func, astroid.Getattr) + or not isinstance(node.func.expr, astroid.Name)): return try: logger_class = [inferred for inferred in node.func.expr.infer() if ( - isinstance(inferred, astng.Instance) + isinstance(inferred, astroid.Instance) and any(ancestor for ancestor in inferred._proxied.ancestors() if ( ancestor.name == 'Logger' and ancestor.parent.name == 'logging')))] - except astng.exceptions.InferenceError: + except astroid.exceptions.InferenceError: return if (node.func.expr.name != self._logging_name and not logger_class): return @@ -103,9 +102,9 @@ def _check_convenience_methods(self, node): # Either no args, star args, or double-star args. Beyond the # scope of this checker. return - if isinstance(node.args[0], astng.BinOp) and node.args[0].op == '%': + if isinstance(node.args[0], astroid.BinOp) and node.args[0].op == '%': self.add_message('W1201', node=node) - elif isinstance(node.args[0], astng.Const): + elif isinstance(node.args[0], astroid.Const): self._check_format_string(node, 0) def _check_log_methods(self, node): @@ -116,9 +115,9 @@ def _check_log_methods(self, node): # Either a malformed call, star args, or double-star args. Beyond # the scope of this checker. return - if isinstance(node.args[1], astng.BinOp) and node.args[1].op == '%': + if isinstance(node.args[1], astroid.BinOp) and node.args[1].op == '%': self.add_message('W1201', node=node) - elif isinstance(node.args[1], astng.Const): + elif isinstance(node.args[1], astroid.Const): self._check_format_string(node, 1) def _check_format_string(self, node, format_arg): @@ -171,7 +170,7 @@ def _count_supplied_tokens(self, args): Returns: Number of AST nodes that aren't keywords. """ - return sum(1 for arg in args if not isinstance(arg, astng.Keyword)) + return sum(1 for arg in args if not isinstance(arg, astroid.Keyword)) def register(linter): diff --git a/pylibs/pylama/checkers/pylint/checkers/misc.py b/pylibs/pylama/checkers/pylint/checkers/misc.py index 8830b8fd..dee53e62 100644 --- a/pylibs/pylama/checkers/pylint/checkers/misc.py +++ b/pylibs/pylama/checkers/pylint/checkers/misc.py @@ -20,19 +20,25 @@ import re from ..interfaces import IRawChecker -from ..checkers import BaseChecker +from . import BaseChecker MSGS = { 'W0511': ('%s', 'fixme', 'Used when a warning note as FIXME or XXX is detected.'), + 'W0512': ('Cannot decode using encoding "%s", unexpected byte at position %d', + 'invalid-encoded-data', + 'Used when a source line cannot be decoded using the specified ' + 'source file encoding.', + {'maxversion': (3, 0)}), } + class EncodingChecker(BaseChecker): """checks for: * warning notes in the code like FIXME, XXX - * PEP 263: source code with non ascii character but no encoding declaration + * encoding issues. """ __implements__ = IRawChecker @@ -48,30 +54,36 @@ class EncodingChecker(BaseChecker): }), ) - def __init__(self, linter=None): - BaseChecker.__init__(self, linter) + def _check_note(self, notes, lineno, line): + match = notes.search(line) + if match: + self.add_message('W0511', args=line[match.start():-1], line=lineno) + + def _check_encoding(self, lineno, line, file_encoding): + try: + return unicode(line, file_encoding) + except UnicodeDecodeError, ex: + self.add_message('W0512', line=lineno, + args=(file_encoding, ex.args[2])) - def process_module(self, node): - """inspect the source file to found encoding problem or fixmes like + def process_module(self, module): + """inspect the source file to find encoding problem or fixmes like notes """ - stream = node.file_stream - stream.seek(0) # XXX may be removed with astng > 0.23 - # warning notes in the code - notes = [] - for note in self.config.notes: - notes.append(re.compile(note)) - linenum = 1 - for line in stream.readlines(): - for note in notes: - match = note.search(line) - if match: - self.add_message('W0511', args=line[match.start():-1], - line=linenum) - break - linenum += 1 - - + stream = module.file_stream + stream.seek(0) # XXX may be removed with astroid > 0.23 + if self.config.notes: + notes = re.compile('|'.join(self.config.notes)) + else: + notes = None + if module.file_encoding: + encoding = module.file_encoding + else: + encoding = 'ascii' + for lineno, line in enumerate(stream): + line = self._check_encoding(lineno+1, line, encoding) + if line is not None and notes: + self._check_note(notes, lineno+1, line) def register(linter): """required method to auto register this checker""" diff --git a/pylibs/pylama/checkers/pylint/checkers/newstyle.py b/pylibs/pylama/checkers/pylint/checkers/newstyle.py index c72f8d0d..715282a9 100644 --- a/pylibs/pylama/checkers/pylint/checkers/newstyle.py +++ b/pylibs/pylama/checkers/pylint/checkers/newstyle.py @@ -15,12 +15,13 @@ # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """check for new / old style related problems """ +import sys -from ..logilab import astng +from .. import astroid -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker -from ..checkers.utils import check_messages +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import check_messages MSGS = { 'E1001': ('Use of __slots__ on an old style class', @@ -29,15 +30,23 @@ 'E1002': ('Use of super on an old style class', 'super-on-old-class', 'Used when an old style class uses the super builtin.'), - 'E1003': ('Bad first argument %r given to super class', + 'E1003': ('Bad first argument %r given to super()', 'bad-super-call', 'Used when another argument than the current class is given as \ first argument of the super builtin.'), + 'E1004': ('Missing argument to super()', + 'missing-super-argument', + 'Used when the super builtin didn\'t receive an \ + argument on Python 2'), 'W1001': ('Use of "property" on an old style class', 'property-on-old-class', 'Used when PyLint detect the use of the builtin "property" \ on an old style class while this is relying on new style \ classes features'), + 'C1001': ('Old-style class defined.', + 'old-style-class', + 'Used when a class is defined that does not inherit from another' + 'class and does not inherit explicitly from "object".') } @@ -48,7 +57,7 @@ class NewStyleConflictChecker(BaseChecker): * "super" usage """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) # configuration section name name = 'newstyle' @@ -58,53 +67,67 @@ class NewStyleConflictChecker(BaseChecker): # configuration options options = () - @check_messages('E1001') + @check_messages('E1001', 'C1001') def visit_class(self, node): """check __slots__ usage """ if '__slots__' in node and not node.newstyle: self.add_message('E1001', node=node) + # The node type could be class, exception, metaclass, or + # interface. Presumably, the non-class-type nodes would always + # have an explicit base class anyway. + if not node.bases and node.type == 'class': + self.add_message('C1001', node=node) @check_messages('W1001') def visit_callfunc(self, node): """check property usage""" parent = node.parent.frame() - if (isinstance(parent, astng.Class) and + if (isinstance(parent, astroid.Class) and not parent.newstyle and - isinstance(node.func, astng.Name)): + isinstance(node.func, astroid.Name)): name = node.func.name if name == 'property': self.add_message('W1001', node=node) - @check_messages('E1002', 'E1003') + @check_messages('E1002', 'E1003', 'E1004') def visit_function(self, node): """check use of super""" # ignore actual functions or method within a new style class if not node.is_method(): return klass = node.parent.frame() - for stmt in node.nodes_of_class(astng.CallFunc): + for stmt in node.nodes_of_class(astroid.CallFunc): expr = stmt.func - if not isinstance(expr, astng.Getattr): + if not isinstance(expr, astroid.Getattr): continue call = expr.expr # skip the test if using super - if isinstance(call, astng.CallFunc) and \ - isinstance(call.func, astng.Name) and \ + if isinstance(call, astroid.CallFunc) and \ + isinstance(call.func, astroid.Name) and \ call.func.name == 'super': if not klass.newstyle: # super should not be used on an old style class self.add_message('E1002', node=node) else: # super first arg should be the class + if not call.args and sys.version_info[0] == 3: + # unless Python 3 + continue + try: supcls = (call.args and call.args[0].infer().next() or None) - except astng.InferenceError: + except astroid.InferenceError: continue + + if supcls is None and sys.version_info[0] == 2: + self.add_message('missing-super-argument', node=call) + continue + if klass is not supcls: supcls = getattr(supcls, 'name', supcls) - self.add_message('E1003', node=node, args=supcls) + self.add_message('E1003', node=call, args=(supcls, )) def register(linter): diff --git a/pylibs/pylama/checkers/pylint/checkers/raw_metrics.py b/pylibs/pylama/checkers/pylint/checkers/raw_metrics.py index 9f270357..4d450da1 100644 --- a/pylibs/pylama/checkers/pylint/checkers/raw_metrics.py +++ b/pylibs/pylama/checkers/pylint/checkers/raw_metrics.py @@ -1,3 +1,6 @@ +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -24,8 +27,9 @@ from ..logilab.common.ureports import Table -from ..interfaces import IRawChecker -from ..checkers import BaseRawChecker, EmptyReport +from ..interfaces import ITokenChecker +from ..utils import EmptyReport +from ..checkers import BaseTokenChecker from ..reporters import diff_string def report_raw_stats(sect, stats, old_stats): @@ -50,7 +54,7 @@ def report_raw_stats(sect, stats, old_stats): sect.append(Table(children=lines, cols=5, rheaders=1)) -class RawMetricsChecker(BaseRawChecker): +class RawMetricsChecker(BaseTokenChecker): """does not check anything but gives some raw metrics : * total number of lines * total number of code lines @@ -59,7 +63,7 @@ class RawMetricsChecker(BaseRawChecker): * total number of empty lines """ - __implements__ = (IRawChecker,) + __implements__ = (ITokenChecker,) # configuration section name name = 'metrics' @@ -71,7 +75,7 @@ class RawMetricsChecker(BaseRawChecker): reports = ( ('RP0701', 'Raw metrics', report_raw_stats), ) def __init__(self, linter): - BaseRawChecker.__init__(self, linter) + BaseTokenChecker.__init__(self, linter) self.stats = None def open(self): diff --git a/pylibs/pylama/checkers/pylint/checkers/similar.py b/pylibs/pylama/checkers/pylint/checkers/similar.py index 40d8a123..f22116e9 100644 --- a/pylibs/pylama/checkers/pylint/checkers/similar.py +++ b/pylibs/pylama/checkers/pylint/checkers/similar.py @@ -1,5 +1,5 @@ # pylint: disable=W0622 -# Copyright (c) 2004-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2004-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -22,10 +22,10 @@ from ..logilab.common.ureports import Table from ..interfaces import IRawChecker -from ..checkers import BaseChecker, table_lines_from_stats +from . import BaseChecker, table_lines_from_stats -class Similar: +class Similar(object): """finds copy-pasted lines of code in a project""" def __init__(self, min_lines=4, ignore_comments=False, @@ -36,14 +36,21 @@ def __init__(self, min_lines=4, ignore_comments=False, self.ignore_imports = ignore_imports self.linesets = [] - def append_stream(self, streamid, stream): + def append_stream(self, streamid, stream, encoding=None): """append a file to search for similarities""" - stream.seek(0) # XXX may be removed with astng > 0.23 - self.linesets.append(LineSet(streamid, - stream.readlines(), - self.ignore_comments, - self.ignore_docstrings, - self.ignore_imports)) + stream.seek(0) # XXX may be removed with astroid > 0.23 + if encoding is None: + readlines = stream.readlines + else: + readlines = lambda: [line.decode(encoding) for line in stream] + try: + self.linesets.append(LineSet(streamid, + readlines(), + self.ignore_comments, + self.ignore_docstrings, + self.ignore_imports)) + except UnicodeDecodeError: + pass def run(self): """start looking for similarities and display results on stdout""" @@ -80,7 +87,7 @@ def _display_sims(self, sims): print "==%s:%s" % (lineset.name, idx) # pylint: disable=W0631 for line in lineset._real_lines[idx:idx+num]: - print " ", line, + print " ", line.rstrip() nb_lignes_dupliquees += num * (len(couples)-1) nb_total_lignes = sum([len(lineset) for lineset in self.linesets]) print "TOTAL lines=%s duplicates=%s percent=%.2f" \ @@ -153,7 +160,8 @@ def stripped_lines(lines, ignore_comments, ignore_docstrings, ignore_imports): strippedlines.append(line) return strippedlines -class LineSet: + +class LineSet(object): """Holds and indexes all the lines of a single source file""" def __init__(self, name, lines, ignore_comments=False, ignore_docstrings=False, ignore_imports=False): @@ -288,7 +296,7 @@ def process_module(self, node): stream must implement the readlines method """ - self.append_stream(self.linter.current_name, node.file_stream) + self.append_stream(self.linter.current_name, node.file_stream, node.file_encoding) def close(self): """compute and display similarities on closing (i.e. end of parsing)""" diff --git a/pylibs/pylama/checkers/pylint/checkers/stdlib.py b/pylibs/pylama/checkers/pylint/checkers/stdlib.py new file mode 100644 index 00000000..51450983 --- /dev/null +++ b/pylibs/pylama/checkers/pylint/checkers/stdlib.py @@ -0,0 +1,68 @@ +# Copyright 2012 Google Inc. +# +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +"""Checkers for various standard library functions.""" + +import re +import sys + +from .. import astroid + +from ..interfaces import IAstroidChecker +from . import BaseChecker, BaseTokenChecker, utils + +_VALID_OPEN_MODE_REGEX = r'^(r?U|[rwa]\+?b?)$' + +if sys.version_info >= (3, 0): + OPEN_MODULE = '_io' +else: + OPEN_MODULE = '__builtin__' + +class OpenModeChecker(BaseChecker): + __implements__ = (IAstroidChecker,) + name = 'open_mode' + + msgs = { + 'W1501': ('"%s" is not a valid mode for open.', + 'bad-open-mode', + 'Python supports: r, w, a modes with b, +, and U options. ' + 'See http://docs.python.org/2/library/functions.html#open'), + } + + @utils.check_messages('W1501') + def visit_callfunc(self, node): + """Visit a CallFunc node.""" + if hasattr(node, 'func'): + infer = utils.safe_infer(node.func) + if infer and infer.root().name == OPEN_MODULE: + if getattr(node.func, 'name', None) in ('open', 'file'): + self._check_open_mode(node) + + def _check_open_mode(self, node): + """Check that the mode argument of an open or file call is valid.""" + try: + mode_arg = utils.get_argument_from_call(node, position=1, keyword='mode') + if mode_arg: + mode_arg = utils.safe_infer(mode_arg) + if (isinstance(mode_arg, astroid.Const) + and not re.match(_VALID_OPEN_MODE_REGEX, mode_arg.value)): + self.add_message('W1501', node=node, args=(mode_arg.value)) + except (utils.NoSuchArgumentError, TypeError): + pass + +def register(linter): + """required method to auto register this checker """ + linter.register_checker(OpenModeChecker(linter)) + diff --git a/pylibs/pylama/checkers/pylint/checkers/strings.py b/pylibs/pylama/checkers/pylint/checkers/strings.py index 3c755dd7..3d5e8051 100644 --- a/pylibs/pylama/checkers/pylint/checkers/strings.py +++ b/pylibs/pylama/checkers/pylint/checkers/strings.py @@ -21,11 +21,10 @@ import sys import tokenize -from ..logilab import astng +from .. import astroid -from ..interfaces import IRawChecker, IASTNGChecker -from ..checkers import BaseChecker, BaseRawChecker -from ..checkers import utils +from ..interfaces import ITokenChecker, IAstroidChecker +from . import BaseChecker, BaseTokenChecker, utils _PY3K = sys.version_info >= (3, 0) @@ -72,26 +71,27 @@ specifiers is given too many arguments"), } -OTHER_NODES = (astng.Const, astng.List, astng.Backquote, - astng.Lambda, astng.Function, - astng.ListComp, astng.SetComp, astng.GenExpr) +OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote, + astroid.Lambda, astroid.Function, + astroid.ListComp, astroid.SetComp, astroid.GenExpr) class StringFormatChecker(BaseChecker): """Checks string formatting operations to ensure that the format string is valid and the arguments match the format string. """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) name = 'string' msgs = MSGS + @utils.check_messages(*(MSGS.keys())) def visit_binop(self, node): if node.op != '%': return left = node.left args = node.right - if not (isinstance(left, astng.Const) + if not (isinstance(left, astroid.Const) and isinstance(left.value, basestring)): return format_string = left.value @@ -114,11 +114,11 @@ def visit_binop(self, node): # Check that the RHS of the % operator is a mapping object # that contains precisely the set of keys required by the # format string. - if isinstance(args, astng.Dict): + if isinstance(args, astroid.Dict): keys = set() unknown_keys = False for k, _ in args.items: - if isinstance(k, astng.Const): + if isinstance(k, astroid.Const): key = k.value if isinstance(key, basestring): keys.add(key) @@ -137,7 +137,7 @@ def visit_binop(self, node): for key in keys: if key not in required_keys: self.add_message('W1301', node=node, args=key) - elif isinstance(args, OTHER_NODES + (astng.Tuple,)): + elif isinstance(args, OTHER_NODES + (astroid.Tuple,)): type_name = type(args).__name__ self.add_message('E1303', node=node, args=type_name) # else: @@ -149,9 +149,9 @@ def visit_binop(self, node): # Check that the number of arguments passed to the RHS of # the % operator matches the number required by the format # string. - if isinstance(args, astng.Tuple): + if isinstance(args, astroid.Tuple): num_args = len(args.elts) - elif isinstance(args, OTHER_NODES + (astng.Dict, astng.DictComp)): + elif isinstance(args, OTHER_NODES + (astroid.Dict, astroid.DictComp)): num_args = 1 else: # The RHS of the format specifier is a name or @@ -166,7 +166,7 @@ def visit_binop(self, node): class StringMethodsChecker(BaseChecker): - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) name = 'string' msgs = { 'E1310': ("Suspicious argument in %s.%s call", @@ -175,24 +175,25 @@ class StringMethodsChecker(BaseChecker): " duplicate character, "), } + @utils.check_messages(*(MSGS.keys())) def visit_callfunc(self, node): func = utils.safe_infer(node.func) - if (isinstance(func, astng.BoundMethod) - and isinstance(func.bound, astng.Instance) + if (isinstance(func, astroid.BoundMethod) + and isinstance(func.bound, astroid.Instance) and func.bound.name in ('str', 'unicode', 'bytes') and func.name in ('strip', 'lstrip', 'rstrip') and node.args): arg = utils.safe_infer(node.args[0]) - if not isinstance(arg, astng.Const): + if not isinstance(arg, astroid.Const): return if len(arg.value) != len(set(arg.value)): self.add_message('E1310', node=node, args=(func.bound.name, func.name)) -class StringConstantChecker(BaseRawChecker): +class StringConstantChecker(BaseTokenChecker): """Check string literals""" - __implements__ = (IRawChecker, IASTNGChecker) + __implements__ = (ITokenChecker,) name = 'string_constant' msgs = { 'W1401': ('Anomalous backslash in string: \'%s\'. ' diff --git a/pylibs/pylama/checkers/pylint/checkers/typecheck.py b/pylibs/pylama/checkers/pylint/checkers/typecheck.py index 4cdc6006..e8938827 100644 --- a/pylibs/pylama/checkers/pylint/checkers/typecheck.py +++ b/pylibs/pylama/checkers/pylint/checkers/typecheck.py @@ -13,18 +13,18 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -"""try to find more bugs in the code using astng inference capabilities +"""try to find more bugs in the code using astroid inference capabilities """ import re import shlex -from ..logilab import astng -from ..logilab.astng import InferenceError, NotFoundError, YES, Instance +from .. import astroid +from ..astroid import InferenceError, NotFoundError, YES, Instance -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker -from ..checkers.utils import safe_infer, is_super, check_messages +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import safe_infer, is_super, check_messages MSGS = { 'E1101': ('%s %r has no %r member', @@ -37,7 +37,7 @@ 'E1103': ('%s %r has no %r member (but some types could not be inferred)', 'maybe-no-member', 'Used when a variable is accessed for an unexistent member, but \ - astng was not able to interpret all possible types of this \ + astroid was not able to interpret all possible types of this \ variable.'), 'E1111': ('Assigning to function call which doesn\'t return', 'assignment-from-no-return', @@ -58,7 +58,8 @@ 'E1122': ('Duplicate keyword argument %r in function call', 'duplicate-keyword-arg', 'Used when a function call passes the same keyword argument \ - multiple times.'), + multiple times.', + {'maxversion': (2, 6)}), 'E1123': ('Passing unexpected keyword argument %r in function call', 'unexpected-keyword-arg', 'Used when a function call passes a keyword argument that \ @@ -68,13 +69,18 @@ 'Used when a function call would result in assigning multiple \ values to a function parameter, one value from a positional \ argument and one from a keyword argument.'), + 'E1125': ('Missing mandatory keyword argument %r', + 'missing-kwoa', + 'Used when a function call doesn\'t pass a mandatory \ + keyword-only argument.', + {'minversion': (3, 0)}), } class TypeChecker(BaseChecker): """try to find bugs in the code using type inference """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) # configuration section name name = 'typecheck' @@ -120,7 +126,7 @@ def open(self): self.generated_members.extend(('REQUEST', 'acl_users', 'aq_parent')) def visit_assattr(self, node): - if isinstance(node.ass_type(), astng.AugAssign): + if isinstance(node.ass_type(), astroid.AugAssign): self.visit_getattr(node) def visit_delattr(self, node): @@ -162,7 +168,7 @@ def visit_getattr(self, node): inference_failure = True continue # skip None anyway - if isinstance(owner, astng.Const) and owner.value is None: + if isinstance(owner, astroid.Const) and owner.value is None: continue # XXX "super" / metaclass call if is_super(owner) or getattr(owner, 'type', None) == 'metaclass': @@ -174,14 +180,14 @@ def visit_getattr(self, node): continue try: if not [n for n in owner.getattr(node.attrname) - if not isinstance(n.statement(), astng.AugAssign)]: + if not isinstance(n.statement(), astroid.AugAssign)]: missingattr.add((owner, name)) continue except AttributeError: # XXX method / function continue except NotFoundError: - if isinstance(owner, astng.Function) and owner.decorators: + if isinstance(owner, astroid.Function) and owner.decorators: continue if isinstance(owner, Instance) and owner.has_dynamic_getattr(): continue @@ -212,33 +218,34 @@ def visit_getattr(self, node): args=(owner.display_type(), name, node.attrname)) - + @check_messages('E1111', 'W1111') def visit_assign(self, node): """check that if assigning to a function call, the function is possibly returning something valuable """ - if not isinstance(node.value, astng.CallFunc): + if not isinstance(node.value, astroid.CallFunc): return function_node = safe_infer(node.value.func) # skip class, generator and incomplete function definition - if not (isinstance(function_node, astng.Function) and + if not (isinstance(function_node, astroid.Function) and function_node.root().fully_defined()): return if function_node.is_generator() \ or function_node.is_abstract(pass_is_abstract=False): return - returns = list(function_node.nodes_of_class(astng.Return, - skip_klass=astng.Function)) + returns = list(function_node.nodes_of_class(astroid.Return, + skip_klass=astroid.Function)) if len(returns) == 0: self.add_message('E1111', node=node) else: for rnode in returns: - if not (isinstance(rnode.value, astng.Const) + if not (isinstance(rnode.value, astroid.Const) and rnode.value.value is None): break else: self.add_message('W1111', node=node) + @check_messages(*(MSGS.keys())) def visit_callfunc(self, node): """check that called functions/methods are inferred to callable objects, and that the arguments passed to the function match the parameters in @@ -250,7 +257,7 @@ def visit_callfunc(self, node): keyword_args = set() num_positional_args = 0 for arg in node.args: - if isinstance(arg, astng.Keyword): + if isinstance(arg, astroid.Keyword): keyword = arg.arg if keyword in keyword_args: self.add_message('E1122', node=node, args=keyword) @@ -265,18 +272,18 @@ def visit_callfunc(self, node): # Note that BoundMethod is a subclass of UnboundMethod (huh?), so must # come first in this 'if..else'. - if isinstance(called, astng.BoundMethod): + if isinstance(called, astroid.BoundMethod): # Bound methods have an extra implicit 'self' argument. num_positional_args += 1 - elif isinstance(called, astng.UnboundMethod): + elif isinstance(called, astroid.UnboundMethod): if called.decorators is not None: for d in called.decorators.nodes: - if isinstance(d, astng.Name) and (d.name == 'classmethod'): + if isinstance(d, astroid.Name) and (d.name == 'classmethod'): # Class methods have an extra implicit 'cls' argument. num_positional_args += 1 break - elif (isinstance(called, astng.Function) or - isinstance(called, astng.Lambda)): + elif (isinstance(called, astroid.Function) or + isinstance(called, astroid.Lambda)): pass else: return @@ -295,15 +302,15 @@ def visit_callfunc(self, node): parameters = [] parameter_name_to_index = {} for i, arg in enumerate(called.args.args): - if isinstance(arg, astng.Tuple): + if isinstance(arg, astroid.Tuple): name = None # Don't store any parameter names within the tuple, since those # are not assignable from keyword arguments. else: - if isinstance(arg, astng.Keyword): + if isinstance(arg, astroid.Keyword): name = arg.arg else: - assert isinstance(arg, astng.AssName) + assert isinstance(arg, astroid.AssName) # This occurs with: # def f( (a), (b) ): pass name = arg.name @@ -314,6 +321,15 @@ def visit_callfunc(self, node): defval = None parameters.append([(name, defval), False]) + kwparams = {} + for i, arg in enumerate(called.args.kwonlyargs): + if isinstance(arg, astroid.Keyword): + name = arg.arg + else: + assert isinstance(arg, astroid.AssName) + name = arg.name + kwparams[name] = [called.args.kw_defaults[i], False] + # Match the supplied arguments against the function parameters. # 1. Match the positional arguments. @@ -338,6 +354,12 @@ def visit_callfunc(self, node): self.add_message('E1124', node=node, args=keyword) else: parameters[i][1] = True + elif keyword in kwparams: + if kwparams[keyword][1]: # XXX is that even possible? + # Duplicate definition of function parameter. + self.add_message('E1124', node=node, args=keyword) + else: + kwparams[keyword][1] = True elif called.args.kwarg is not None: # The keyword argument gets assigned to the **kwargs parameter. pass @@ -382,6 +404,12 @@ def visit_callfunc(self, node): display_name = repr(name) self.add_message('E1120', node=node, args=display_name) + for name in kwparams: + defval, assigned = kwparams[name] + if defval is None and not assigned: + self.add_message('E1125', node=node, args=name) + + def register(linter): """required method to auto register this checker """ linter.register_checker(TypeChecker(linter)) diff --git a/pylibs/pylama/checkers/pylint/checkers/utils.py b/pylibs/pylama/checkers/pylint/checkers/utils.py index 34d335bb..1a7dca9e 100644 --- a/pylibs/pylama/checkers/pylint/checkers/utils.py +++ b/pylibs/pylama/checkers/pylint/checkers/utils.py @@ -21,19 +21,23 @@ import re import string -from ..logilab import astng -from ..logilab.astng import scoped_nodes +from .. import astroid + +from ..astroid import scoped_nodes from ..logilab.common.compat import builtins BUILTINS_NAME = builtins.__name__ -COMP_NODE_TYPES = astng.ListComp, astng.SetComp, astng.DictComp, astng.GenExpr +COMP_NODE_TYPES = astroid.ListComp, astroid.SetComp, astroid.DictComp, astroid.GenExpr + +class NoSuchArgumentError(Exception): + pass def is_inside_except(node): """Returns true if node is inside the name of an except handler.""" current = node - while current and not isinstance(current.parent, astng.ExceptHandler): + while current and not isinstance(current.parent, astroid.ExceptHandler): current = current.parent return current and current is current.parent.name @@ -41,7 +45,7 @@ def is_inside_except(node): def get_all_elements(node): """Recursively returns all atoms in nested lists and tuples.""" - if isinstance(node, (astng.Tuple, astng.List)): + if isinstance(node, (astroid.Tuple, astroid.List)): for child in node.elts: for e in get_all_elements(child): yield e @@ -56,9 +60,9 @@ def clobber_in_except(node): Returns (True, args for W0623) if assignment clobbers an existing variable, (False, None) otherwise. """ - if isinstance(node, astng.AssAttr): + if isinstance(node, astroid.AssAttr): return (True, (node.attrname, 'object %r' % (node.expr.name,))) - elif isinstance(node, astng.AssName): + elif isinstance(node, astroid.AssName): name = node.name if is_builtin(name): return (True, (name, 'builtins')) @@ -66,7 +70,7 @@ def clobber_in_except(node): scope, stmts = node.lookup(name) if (stmts and not isinstance(stmts[0].ass_type(), - (astng.Assign, astng.AugAssign, astng.ExceptHandler))): + (astroid.Assign, astroid.AugAssign, astroid.ExceptHandler))): return (True, (name, 'outer scope (line %s)' % (stmts[0].fromlineno,))) return (False, None) @@ -79,12 +83,12 @@ def safe_infer(node): try: inferit = node.infer() value = inferit.next() - except astng.InferenceError: + except astroid.InferenceError: return try: inferit.next() return # None if there is ambiguity on the inferred node - except astng.InferenceError: + except astroid.InferenceError: return # there is some kind of ambiguity except StopIteration: return value @@ -100,24 +104,28 @@ def is_super(node): def is_error(node): """return true if the function does nothing but raising an exception""" for child_node in node.get_children(): - if isinstance(child_node, astng.Raise): + if isinstance(child_node, astroid.Raise): return True return False def is_raising(body): """return true if the given statement node raise an exception""" for node in body: - if isinstance(node, astng.Raise): + if isinstance(node, astroid.Raise): return True return False def is_empty(body): """return true if the given node does nothing but 'pass'""" - return len(body) == 1 and isinstance(body[0], astng.Pass) + return len(body) == 1 and isinstance(body[0], astroid.Pass) builtins = builtins.__dict__.copy() SPECIAL_BUILTINS = ('__builtins__',) # '__path__', '__file__') +def is_builtin_object(node): + """Returns True if the given node is an object from the __builtin__ module.""" + return node and node.root().name == '__builtin__' + def is_builtin(name): # was is_native_builtin """return true if could be considered as a builtin defined by python """ @@ -136,20 +144,20 @@ def is_defined_before(var_node): _node = var_node.parent while _node: if isinstance(_node, COMP_NODE_TYPES): - for ass_node in _node.nodes_of_class(astng.AssName): + for ass_node in _node.nodes_of_class(astroid.AssName): if ass_node.name == varname: return True - elif isinstance(_node, astng.For): - for ass_node in _node.target.nodes_of_class(astng.AssName): + elif isinstance(_node, astroid.For): + for ass_node in _node.target.nodes_of_class(astroid.AssName): if ass_node.name == varname: return True - elif isinstance(_node, astng.With): - if _node.vars is None: - # quickfix : case in which 'with' is used without 'as' - return False - if _node.vars.name == varname: - return True - elif isinstance(_node, (astng.Lambda, astng.Function)): + elif isinstance(_node, astroid.With): + for expr, vars in _node.items: + if expr.parent_of(var_node): + break + if vars and vars.name == varname: + return True + elif isinstance(_node, (astroid.Lambda, astroid.Function)): if _node.args.is_argument(varname): return True if getattr(_node, 'name', None) == varname: @@ -161,10 +169,10 @@ def is_defined_before(var_node): _node = stmt.previous_sibling() lineno = stmt.fromlineno while _node and _node.fromlineno == lineno: - for ass_node in _node.nodes_of_class(astng.AssName): + for ass_node in _node.nodes_of_class(astroid.AssName): if ass_node.name == varname: return True - for imp_node in _node.nodes_of_class( (astng.From, astng.Import)): + for imp_node in _node.nodes_of_class( (astroid.From, astroid.Import)): if varname in [name[1] or name[0] for name in imp_node.names]: return True _node = _node.previous_sibling() @@ -175,9 +183,9 @@ def is_func_default(node): value """ parent = node.scope() - if isinstance(parent, astng.Function): + if isinstance(parent, astroid.Function): for default_node in parent.args.defaults: - for default_name_node in default_node.nodes_of_class(astng.Name): + for default_name_node in default_node.nodes_of_class(astroid.Name): if default_name_node is node: return True return False @@ -186,10 +194,10 @@ def is_func_decorator(node): """return true if the name is used in function decorator""" parent = node.parent while parent is not None: - if isinstance(parent, astng.Decorators): + if isinstance(parent, astroid.Decorators): return True if (parent.is_statement or - isinstance(parent, astng.Lambda) or + isinstance(parent, astroid.Lambda) or isinstance(parent, (scoped_nodes.ComprehensionScope, scoped_nodes.ListComp))): break @@ -197,7 +205,7 @@ def is_func_decorator(node): return False def is_ancestor_name(frame, node): - """return True if `frame` is a astng.Class node with `node` in the + """return True if `frame` is a astroid.Class node with `node` in the subtree of its bases attribute """ try: @@ -205,23 +213,23 @@ def is_ancestor_name(frame, node): except AttributeError: return False for base in bases: - if node in base.nodes_of_class(astng.Name): + if node in base.nodes_of_class(astroid.Name): return True return False def assign_parent(node): """return the higher parent which is not an AssName, Tuple or List node """ - while node and isinstance(node, (astng.AssName, - astng.Tuple, - astng.List)): + while node and isinstance(node, (astroid.AssName, + astroid.Tuple, + astroid.List)): node = node.parent return node def overrides_an_abstract_method(class_node, name): """return True if pnode is a parent of node""" for ancestor in class_node.ancestors(): - if name in ancestor and isinstance(ancestor[name], astng.Function) and \ + if name in ancestor and isinstance(ancestor[name], astroid.Function) and \ ancestor[name].is_abstract(pass_is_abstract=False): return True return False @@ -229,7 +237,7 @@ def overrides_an_abstract_method(class_node, name): def overrides_a_method(class_node, name): """return True if is a method overridden from an ancestor""" for ancestor in class_node.ancestors(): - if name in ancestor and isinstance(ancestor[name], astng.Function): + if name in ancestor and isinstance(ancestor[name], astroid.Function): return True return False @@ -352,7 +360,7 @@ def node_frame_class(node): """ klass = node.frame() - while klass is not None and not isinstance(klass, astng.Class): + while klass is not None and not isinstance(klass, astroid.Class): if klass.parent is None: klass = None else: @@ -364,8 +372,8 @@ def is_super_call(expr): """return True if expression node is a function call and if function name is super. Check before that you're in a method. """ - return (isinstance(expr, astng.CallFunc) and - isinstance(expr.func, astng.Name) and + return (isinstance(expr, astroid.CallFunc) and + isinstance(expr.func, astroid.Name) and expr.func.name == 'super') def is_attr_private(attrname): @@ -374,3 +382,28 @@ def is_attr_private(attrname): """ regex = re.compile('^_{2,}.*[^_]+_?$') return regex.match(attrname) + +def get_argument_from_call(callfunc_node, position=None, keyword=None): + """Returns the specified argument from a function call. + + :param callfunc_node: Node representing a function call to check. + :param int position: position of the argument. + :param str keyword: the keyword of the argument. + + :returns: The node representing the argument, None if the argument is not found. + :raises ValueError: if both position and keyword are None. + :raises NoSuchArgumentError: if no argument at the provided position or with + the provided keyword. + """ + if not position and not keyword: + raise ValueError('Must specify at least one of: position or keyword.') + try: + if position and not isinstance(callfunc_node.args[position], astroid.Keyword): + return callfunc_node.args[position] + except IndexError as error: + raise NoSuchArgumentError(error) + if keyword: + for arg in callfunc_node.args: + if isinstance(arg, astroid.Keyword) and arg.arg == keyword: + return arg.value + raise NoSuchArgumentError diff --git a/pylibs/pylama/checkers/pylint/checkers/variables.py b/pylibs/pylama/checkers/pylint/checkers/variables.py index 88bd2a51..afe0f945 100644 --- a/pylibs/pylama/checkers/pylint/checkers/variables.py +++ b/pylibs/pylama/checkers/pylint/checkers/variables.py @@ -19,12 +19,12 @@ import sys from copy import copy -from ..logilab import astng -from ..logilab.astng import are_exclusive, builtin_lookup, ASTNGBuildingException +from .. import astroid +from ..astroid import are_exclusive, builtin_lookup, AstroidBuildingException -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker -from ..checkers.utils import (PYMETHODS, is_ancestor_name, is_builtin, +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import (PYMETHODS, is_ancestor_name, is_builtin, is_defined_before, is_error, is_func_default, is_func_decorator, assign_parent, check_messages, is_inside_except, clobber_in_except, get_all_elements) @@ -32,7 +32,7 @@ def in_for_else_branch(parent, stmt): """Returns True if stmt in inside the else branch for a parent For stmt.""" - return (isinstance(parent, astng.For) and + return (isinstance(parent, astroid.For) and any(else_stmt.parent_of(stmt) for else_stmt in parent.orelse)) def overridden_method(klass, name): @@ -45,9 +45,9 @@ def overridden_method(klass, name): meth_node = parent[name] except KeyError: # We have found an ancestor defining but it's not in the local - # dictionary. This may happen with astng built from living objects. + # dictionary. This may happen with astroid built from living objects. return None - if isinstance(meth_node, astng.Function): + if isinstance(meth_node, astroid.Function): return meth_node return None @@ -118,6 +118,12 @@ def overridden_method(klass, name): 'Used when an loop variable (i.e. defined by a for loop or \ a list comprehension or a generator expression) is used outside \ the loop.'), + + 'W0632': ('Possible unbalanced tuple unpacking: ' + 'left side has %d label(s), right side has %d value(s)', + 'unbalanced-tuple-unpacking', + 'Used when there is an unbalanced tuple unpacking in assignment'), + } class VariablesChecker(BaseChecker): @@ -129,7 +135,7 @@ class VariablesChecker(BaseChecker): * __all__ consistency """ - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'variables' msgs = MSGS @@ -140,7 +146,7 @@ class VariablesChecker(BaseChecker): 'help' : 'Tells whether we should check for unused import in \ __init__ files.'}), ("dummy-variables-rgx", - {'default': ('_|dummy'), + {'default': ('_$|dummy'), 'type' :'regexp', 'metavar' : '', 'help' : 'A regular expression matching the beginning of \ the name of dummy variables (i.e. not used).'}), @@ -166,7 +172,7 @@ def visit_module(self, node): # do not print Redefining builtin for additional builtins self.add_message('W0622', args=name, node=stmts[0]) - @check_messages('W0611', 'W0614') + @check_messages('W0611', 'W0614', 'W0622', 'E0603', 'E0604') def leave_module(self, node): """leave module: check globals """ @@ -175,14 +181,14 @@ def leave_module(self, node): # attempt to check for __all__ if defined if '__all__' in node.locals: assigned = node.igetattr('__all__').next() - if assigned is not astng.YES: + if assigned is not astroid.YES: for elt in getattr(assigned, 'elts', ()): try: elt_name = elt.infer().next() - except astng.InferenceError: + except astroid.InferenceError: continue - if not isinstance(elt_name, astng.Const) or not isinstance(elt_name.value, basestring): + if not isinstance(elt_name, astroid.Const) or not isinstance(elt_name.value, basestring): self.add_message('E0604', args=elt.as_string(), node=elt) continue elt_name = elt.value @@ -197,9 +203,9 @@ def leave_module(self, node): return for name, stmts in not_consumed.iteritems(): stmt = stmts[0] - if isinstance(stmt, astng.Import): + if isinstance(stmt, astroid.Import): self.add_message('W0611', args=name, node=stmt) - elif isinstance(stmt, astng.From) and stmt.modname != '__future__': + elif isinstance(stmt, astroid.From) and stmt.modname != '__future__': if stmt.names[0][0] == '*': self.add_message('W0614', args=name, node=stmt) else: @@ -271,9 +277,11 @@ def visit_function(self, node): for name, stmt in node.items(): if is_inside_except(stmt): continue - if name in globs and not isinstance(stmt, astng.Global): + if name in globs and not isinstance(stmt, astroid.Global): line = globs[name][0].fromlineno - self.add_message('W0621', args=(name, line), node=stmt) + dummy_rgx = self.config.dummy_variables_rgx + if not dummy_rgx.match(name): + self.add_message('W0621', args=(name, line), node=stmt) elif is_builtin(name): # do not print Redefining builtin for additional builtins self.add_message('W0622', args=name, node=stmt) @@ -301,7 +309,7 @@ def leave_function(self, node): # ignore names imported by the global statement # FIXME: should only ignore them if it's assigned latter stmt = stmts[0] - if isinstance(stmt, astng.Global): + if isinstance(stmt, astroid.Global): continue # care about functions with unknown argument (builtins) if name in argnames: @@ -328,7 +336,7 @@ def leave_function(self, node): def visit_global(self, node): """check names imported exists in the global scope""" frame = node.frame() - if isinstance(frame, astng.Module): + if isinstance(frame, astroid.Module): self.add_message('W0604', node=node) return module = frame.root() @@ -336,7 +344,7 @@ def visit_global(self, node): for name in node.names: try: assign_nodes = module.getattr(name) - except astng.NotFoundError: + except astroid.NotFoundError: # unassigned global, skip assign_nodes = [] for anode in assign_nodes: @@ -399,10 +407,11 @@ def _loopvar_name(self, node, name): astmts = _astmts if len(astmts) == 1: ass = astmts[0].ass_type() - if isinstance(ass, (astng.For, astng.Comprehension, astng.GenExpr)) \ + if isinstance(ass, (astroid.For, astroid.Comprehension, astroid.GenExpr)) \ and not ass.statement() is node.statement(): self.add_message('W0631', args=name, node=node) + @check_messages('W0623') def visit_excepthandler(self, node): for name in get_all_elements(node.name): clobbering, args = clobber_in_except(name) @@ -410,19 +419,20 @@ def visit_excepthandler(self, node): self.add_message('W0623', args=args, node=name) def visit_assname(self, node): - if isinstance(node.ass_type(), astng.AugAssign): + if isinstance(node.ass_type(), astroid.AugAssign): self.visit_name(node) def visit_delname(self, node): self.visit_name(node) + @check_messages(*(MSGS.keys())) def visit_name(self, node): """check that a name is defined if the current scope and doesn't redefine a built-in """ stmt = node.statement() if stmt.fromlineno is None: - # name node from a astng built from live code, skip + # name node from a astroid built from live code, skip assert not stmt.root().file.endswith('.py') return name = node.name @@ -481,13 +491,13 @@ def visit_name(self, node): and stmt.fromlineno <= defstmt.fromlineno and not is_defined_before(node) and not are_exclusive(stmt, defstmt, ('NameError', 'Exception', 'BaseException'))): - if defstmt is stmt and isinstance(node, (astng.DelName, - astng.AssName)): + if defstmt is stmt and isinstance(node, (astroid.DelName, + astroid.AssName)): self.add_message('E0602', args=name, node=node) elif self._to_consume[-1][-1] != 'lambda': # E0601 may *not* occurs in lambda scope self.add_message('E0601', args=name, node=node) - if not isinstance(node, astng.AssName): # Aug AssName + if not isinstance(node, astroid.AssName): # Aug AssName del to_consume[name] else: del consumed[name] @@ -497,7 +507,7 @@ def visit_name(self, node): else: # we have not found the name, if it isn't a builtin, that's an # undefined name ! - if not (name in astng.Module.scope_attrs or is_builtin(name) + if not (name in astroid.Module.scope_attrs or is_builtin(name) or name in self.config.additional_builtins): self.add_message('E0602', args=name, node=node) @@ -508,7 +518,7 @@ def visit_import(self, node): parts = name.split('.') try: module = node.infer_name_module(parts[0]).next() - except astng.ResolveError: + except astroid.ResolveError: continue self._check_module_attrs(node, module, parts[1:]) @@ -519,7 +529,7 @@ def visit_from(self, node): level = getattr(node, 'level', None) try: module = node.root().import_module(name_parts[0], level=level) - except ASTNGBuildingException: + except AstroidBuildingException: return except Exception, exc: print 'Unhandled exception in VariablesChecker:', exc @@ -532,12 +542,33 @@ def visit_from(self, node): continue self._check_module_attrs(node, module, name.split('.')) + @check_messages('unbalanced-tuple-unpacking') + def visit_assign(self, node): + """Check unbalanced tuple unpacking for assignments""" + if not isinstance(node.targets[0], (astroid.Tuple, astroid.List)): + return + try: + infered = node.value.infer().next() + except astroid.InferenceError: + return + if not isinstance(infered, (astroid.Tuple, astroid.List)): + return + targets = node.targets[0].itered() + values = infered.itered() + if any(not isinstance(target_node, astroid.AssName) + for target_node in targets): + return + if len(targets) != len(values): + self.add_message('unbalanced-tuple-unpacking', + node=node, + args=(len(targets), len(values))) + def _check_module_attrs(self, node, module, module_names): """check that module_names (list of string) are accessible through the given module if the latest access name corresponds to a module, return it """ - assert isinstance(module, astng.Module), module + assert isinstance(module, astroid.Module), module while module_names: name = module_names.pop(0) if name == '__dict__': @@ -545,12 +576,12 @@ def _check_module_attrs(self, node, module, module_names): break try: module = module.getattr(name)[0].infer().next() - if module is astng.YES: + if module is astroid.YES: return None - except astng.NotFoundError: + except astroid.NotFoundError: self.add_message('E0611', args=(name, module.name), node=node) return None - except astng.InferenceError: + except astroid.InferenceError: return None if module_names: # FIXME: other message if name is not the latest part of @@ -559,7 +590,7 @@ def _check_module_attrs(self, node, module, module_names): self.add_message('E0611', node=node, args=('.'.join(module_names), modname)) return None - if isinstance(module, astng.Module): + if isinstance(module, astroid.Module): return module return None diff --git a/pylibs/pylama/checkers/pylint/config.py b/pylibs/pylama/checkers/pylint/config.py index 94d44b8f..192f2548 100644 --- a/pylibs/pylama/checkers/pylint/config.py +++ b/pylibs/pylama/checkers/pylint/config.py @@ -11,7 +11,7 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -"""utilities for PyLint configuration : +"""utilities for Pylint configuration : * pylintrc * pylint.d (PYLINTHOME) @@ -105,13 +105,13 @@ def find_pylintrc(): PYLINTRC = find_pylintrc() ENV_HELP = ''' -The following environment variables are used: - * PYLINTHOME - path to the directory where data of persistent run will be stored. If not -found, it defaults to ~/.pylint.d/ or .pylint.d (in the current working -directory). - * PYLINTRC - path to the configuration file. If not found, it will use the first +The following environment variables are used: + * PYLINTHOME + path to the directory where data of persistent run will be stored. If not +found, it defaults to ~/.pylint.d/ or .pylint.d (in the current working +directory). + * PYLINTRC + path to the configuration file. If not found, it will use the first existing file among (~/.pylintrc, /etc/pylintrc). ''' % globals() diff --git a/pylibs/pylama/checkers/pylint/interfaces.py b/pylibs/pylama/checkers/pylint/interfaces.py index 2d2432df..7193c65a 100644 --- a/pylibs/pylama/checkers/pylint/interfaces.py +++ b/pylibs/pylama/checkers/pylint/interfaces.py @@ -10,13 +10,7 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -""" Copyright (c) 2002-2003 LOGILAB S.A. (Paris, FRANCE). - http://www.logilab.fr/ -- mailto:contact@logilab.fr - -Interfaces for PyLint objects -""" - -__revision__ = "$Id: interfaces.py,v 1.9 2004-04-24 12:14:53 syt Exp $" +"""Interfaces for PyLint objects""" from .logilab.common.interface import Interface @@ -32,51 +26,31 @@ def open(self): def close(self): """called after visiting project (i.e set of modules)""" -## def open_module(self): -## """called before visiting a module""" - -## def close_module(self): -## """called after visiting a module""" - class IRawChecker(IChecker): """interface for checker which need to parse the raw file """ - def process_module(self, astng): + def process_module(self, astroid): """ process a module - the module's content is accessible via astng.file_stream + the module's content is accessible via astroid.file_stream """ -class IASTNGChecker(IChecker): - """ interface for checker which prefers receive events according to - statement type - """ - - -class ILinter(Interface): - """interface for the linter class +class ITokenChecker(IChecker): + """Interface for checkers that need access to the token list.""" + def process_tokens(self, tokens): + """Process a module. - the linter class will generate events to its registered checkers. - Each checker may interact with the linter instance using this API - """ - - def register_checker(self, checker): - """register a new checker class - - checker is a class implementing IrawChecker or / and IASTNGChecker + tokens is a list of all source code tokens in the file. """ - def add_message(self, msg_id, line=None, node=None, args=None): - """add the message corresponding to the given id. - - If provided, msg is expanded using args - astng checkers should provide the node argument, - raw checkers should provide the line argument. - """ +class IAstroidChecker(IChecker): + """ interface for checker which prefers receive events according to + statement type + """ class IReporter(Interface): @@ -95,4 +69,4 @@ def display_results(self, layout): """ -__all__ = ('IRawChecker', 'ILinter', 'IReporter') +__all__ = ('IRawChecker', 'IAstroidChecker', 'ITokenChecker', 'IReporter') diff --git a/pylibs/pylama/checkers/pylint/lint.py b/pylibs/pylama/checkers/pylint/lint.py index 60a9784b..25c5377b 100644 --- a/pylibs/pylama/checkers/pylint/lint.py +++ b/pylibs/pylama/checkers/pylint/lint.py @@ -31,7 +31,6 @@ import sys import os -import re import tokenize from warnings import warn @@ -43,30 +42,24 @@ from .logilab.common.ureports import Table, Text, Section from .logilab.common.__pkginfo__ import version as common_version -from .logilab.astng import MANAGER, nodes, ASTNGBuildingException -from .logilab.astng.__pkginfo__ import version as astng_version - -from .utils import (PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, - ReportsHandlerMixIn, MSG_TYPES, expand_modules, - WarningScope) -from .interfaces import ILinter, IRawChecker, IASTNGChecker -from .checkers import (BaseRawChecker, EmptyReport, - table_lines_from_stats) -from .reporters.text import (TextReporter, ParseableTextReporter, - VSTextReporter, ColorizedTextReporter) -from .reporters.html import HTMLReporter +from .astroid import MANAGER, nodes, AstroidBuildingException +from .astroid.__pkginfo__ import version as astroid_version + +from .utils import ( + MSG_TYPES, OPTION_RGX, + PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, + EmptyReport, WarningScope, + expand_modules, tokenize_module) +from .interfaces import IRawChecker, ITokenChecker, IAstroidChecker +from .checkers import (BaseTokenChecker, + table_lines_from_stats, + initialize as checkers_initialize) +from .reporters import initialize as reporters_initialize from . import config from .__pkginfo__ import version -OPTION_RGX = re.compile(r'\s*#.*\bpylint:(.*)') -REPORTER_OPT_MAP = {'text': TextReporter, - 'parseable': ParseableTextReporter, - 'msvs': VSTextReporter, - 'colorized': ColorizedTextReporter, - 'html': HTMLReporter,} - def _get_python_path(filepath): dirname = os.path.dirname(os.path.realpath( @@ -88,8 +81,8 @@ def _get_python_path(filepath): 'Used when an error occurred preventing the analysis of a \ module (unable to find it for instance).'), 'F0002': ('%s: %s', - 'astng-error', - 'Used when an unexpected error occurred while building the ASTNG \ + 'astroid-error', + 'Used when an unexpected error occurred while building the Astroid \ representation. This is usually accompanied by a traceback. \ Please report such errors !'), 'F0003': ('ignored builtin module %s', @@ -102,8 +95,8 @@ def _get_python_path(filepath): inferred.'), 'F0010': ('error while code parsing: %s', 'parse-error', - 'Used when an exception occured while building the ASTNG \ - representation which could be handled by astng.'), + 'Used when an exception occured while building the Astroid \ + representation which could be handled by astroid.'), 'I0001': ('Unable to run raw checkers on built-in module %s', 'raw-checker-failed', @@ -157,21 +150,21 @@ def _get_python_path(filepath): class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, - BaseRawChecker): + BaseTokenChecker): """lint Python modules using external checkers. This is the main checker controlling the other ones and the reports - generation. It is itself both a raw checker and an astng checker in order + generation. It is itself both a raw checker and an astroid checker in order to: * handle message activation / deactivation at the module level * handle some basic but necessary stats'data (number of classes, methods...) IDE plugins developpers: you may have to call - `logilab.astng.builder.MANAGER.astng_cache.clear()` accross run if you want + `astroid.builder.MANAGER.astroid_cache.clear()` accross run if you want to ensure the latest code version is actually checked. """ - __implements__ = (ILinter, IRawChecker) + __implements__ = (ITokenChecker,) name = 'master' priority = 0 @@ -206,18 +199,6 @@ def make_options(): 'can also give a reporter class, eg mypackage.mymodule.' 'MyReporterClass.'}), - ('include-ids', - {'type' : 'yn', 'metavar' : '', 'default' : 0, - 'short': 'i', - 'group': 'Reports', - 'help' : 'Include message\'s id in output'}), - - ('symbols', - {'type' : 'yn', 'metavar' : '', 'default' : 0, - 'short': 's', - 'group': 'Reports', - 'help' : 'Include symbolic ids of messages in output'}), - ('files-output', {'default': 0, 'type' : 'yn', 'metavar' : '', 'group': 'Reports', 'level': 1, @@ -274,6 +255,16 @@ def make_options(): 'If you want to run only the classes checker, but have no ' 'Warning level messages displayed, use' '"--disable=all --enable=classes --disable=W"'}), + + ('msg-template', + {'type' : 'string', 'metavar': '